Socket
Socket
Sign inDemoInstall

@hytts/hytts

Package Overview
Dependencies
18
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.1.0 to 0.2.3

192

main.browser.d.ts

@@ -0,96 +1,124 @@

declare const eventRemovalSymbol: unique symbol;
declare global {
interface Element {
[eventRemovalSymbol]?: AbortController[];
}
}
declare function addEventListener(
elementId: string,
eventName: string,
eventListener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void;
/**
* Used to select a frame within the document, which is either the document's body element
* or the id of a `hy-frame` element, in which case the frame id is expected to be unique.
* Used to select a `hy-frame` element within the document. It is assumed that there always is a
* root frame directly below the document's body.
*/
type FrameSelector = string;
type FrameId = string;
/**
* Simulates a full page navigation to a route by first pushing a new entry onto the browser's
* history stack with the route's URL and then loads the route to update the selected frame.
* Updates the contents of the frame with the element returned by the given callback. Ensures that
* only one update can be in-flight concurrently for the frame. If a new update is issued before a
* previous one finished, the previous one is immediately aborted so that it can't have any effect
* on the frame anymore and the new update is started immediately. After a successful update, all
* pending updates for any of the frame's transitive child frames are aborted.
*
* Note that we make the history change first to simulate the native browser behavior. In case of an
* error or a long loading time, the URL is already updated, so a browser reload will then reload the
* new URL instead of the old one.
*
* If the server's response to the route is a redirect, the redirect is followed automatically. In
* that case, the previously pushed history entry is replaced with the new URL after the redirected
* response is fetched. This is different from the normal browser behavior, where the history stack
* is updated before requested the redirected URL. Due to security considerations, this behavior
* cannot be reimplemented with JavaScript.
*
* @param frameSelector Selects the frame that should be updated.
* @param routeUrl The URL of the route that should be loaded and navigated to.
* @param frameId The id of the frame that should be updated.
* @param getFrameElement A callback that returns the HTML element the frame should be updated with.
* If `undefined` is returned, the frame's content remains unmodified. The callback should use the
* given `AbortSignal` to abort the update as soon as possible once a newer update is started.
*/
declare function navigateToRoute(frameSelector: FrameSelector, routeUrl: string): Promise<void>;
declare function updateFrame(
frameId: FrameId,
getFrameElement: (frame: Element, abortSignal: AbortSignal) => Promise<Element | undefined>,
): Promise<void>;
type SubmissionOptions = {
/** The URL that should be submitted to. */
readonly href: string;
/** The form element that should be submitted. */
readonly form: HTMLFormElement;
/** URL-encoded, optional data that is sent in addition to the form data. */
readonly additionalData?: string;
/**
* Selects the frame that should be updated with the request's response. If none is given,
* updates the form's nearest ancestor frame.
*/
readonly frameId?: FrameId;
/**
* If `true`, updates the browser's history stack after successful form submission with either
* `href` or, if the server redirects, the redirect URL. Defaults to `true` for form submissions
* targeting the root frame.
*/
readonly updateHistory?: boolean;
};
declare function submitForm({
href,
form,
additionalData,
frameId,
updateHistory,
}: SubmissionOptions): Promise<void>;
/** The list of HTTP verbs supported by HyTTS. */
declare const httpMethods: readonly ["GET", "POST"];
/** The list of HTTP verbs supported by HyTTS. */
type HttpMethod = (typeof httpMethods)[number];
type NavigationOptions = {
/** The URL that should be navigated to. */
readonly href: string;
/** The HTTP method that should be used for the request to the server. */
readonly httpMethod: HttpMethod;
/** The URL-encoded body parameters that should be sent with the (non-`GET`) request. */
readonly bodyParams?: string;
/**
* The URL that should be pushed onto the history stack if `updateHistory` is `true`. Defaults
* to the `href` prop if `historyHref` is `undefined`.
*/
readonly historyHref?: string;
/**
* If `true`, updates the browser's history stack. Defaults to `true` for requests targeting the
* root frame.
*/
readonly updateHistory?: boolean;
/** Selects the frame that should be updated. */
readonly frameId: FrameId;
};
/**
* Simulates a full page navigation while also executing the given action. The selected frame is
* updated with the action's response HTML. Before the action is invoked, the given route URL is
* pushed onto the browser's history stack. The server must serve a route at this URL, otherwise
* the browser's reload behavior is broken because such a reload always issues a GET request for a
* route instead of a POST request to an action.
* Simulates a full page navigation to a URL by fetching he server-rendered HTML, updating the
* target frame and afterwards, optionally, pushing a new entry onto the browser's history stack.
*
* Note that we make the history change first to simulate the browser behavior. In case of an error
* or a long loading time, the URL is already updated, so a browser reload will then reload the new
* URL instead of the old one. In that case, however, it is unclear whether the action has already
* been carried out, will be carried out eventually, or whether it never reached the server in the
* first place. This behavior is somewhat different from the native browser behavior, which would
* re-issue the POST request again in such a situation, typically after warning the user that she
* is about to send the data again. So also in that case, the user does not know whether the action
* is carried out twice.
* When navigating to a non-`GET` route and a history entry is pushed onto the stack, the server
* must serve a `GET` route at this URL, otherwise the browser's reload behavior is broken because
* such a reload always issues a GET request as opposed to using the original HTTP method.
*
* If the server's response to the route is a redirect, the redirect is followed automatically. In
* that case, the previously pushed history entry is replaced with the new URL after the redirected
* response is fetched. This is different from the normal browser behavior, where the history stack
* is updated before requested the redirected URL. Due to security considerations, this behavior
* cannot be reimplemented with JavaScript.
* When a navigation is aborted, e.g., due to a browser reload or another frame update, for which
* the server potentially carries out side effects (which typically happens for POST requests), it
* is unclear whether the side effects have already been carried out, will be carried out
* eventually, or whether the request never reached the server in the first place. In this case, the
* HyTTS behavior for browser reloads is somewhat different from the native browser behavior, which
* would re-issue the POST request again, typically after warning the user that she is about to send
* the data again. With HyTTS, by contrast, did not update the browser history before the request
* came through, so a GET request to the URL before the navigation is issues on a browser fresh.
* Neither the browser's behavior nor HyTTS's behavior is ideal in that case, as the user has no
* idea whether or not her "aborted" actions have already been carried out.
*
* @param frameSelector Selects the frame that should be updated.
* @param actionUrl The URL of the action that should be executed.
* @param actionParams The action's URL encoded params.
* @param routeUrlForHistory The route URL that should be pushed onto the browser's history stack.
* If the server's response is a redirect, the redirect is followed automatically, and, optionally,
* the browser's history stack is updated accordingly.
*/
declare function navigateToAction(
frameSelector: FrameSelector,
actionUrl: string,
actionParams: string,
routeUrlForHistory: string
): Promise<void>;
/**
* Loads the HTML for the given `routeUrl` and updates the selected frame accordingly.
* @param frameSelector Selects the frame that should be updated.
* @param routeUrl The URL of the route that should be loaded.
*/
declare function loadRoute(frameSelector: FrameSelector, routeUrl: string): Promise<void>;
/**
* Executes the given action and updates the selected frame based on the returned HTML.
* @param frameSelector Selects the frame that should be updated.
* @param actionUrl The URL of the action that should be executed.
* @param actionParams The action's URL encoded params.
*/
declare function executeAction(
frameSelector: FrameSelector,
actionUrl: string,
actionParams: string
): Promise<void>;
/**
* Updates the contents of the selected frame with the element returned by the given callback.
* Ensures that only one update can be in-flight concurrently for the frame. If a new update is issued
* before a previous one finished, the previous one is immediately aborted so that it can't have any
* effect on the frame anymore and the new update is started immediately. After a successful update,
* all pending updates for any of the frame's transitive child frames are aborted.
* @param frameSelector Selects the frame that should be updated.
* @param getFrameElement A callback that returns the HTML element the frame should be updated with.
* The callback should use the given `AbortSignal` to abort the update as soon
* as possible once a newer update is started.
*/
declare function updateFrame(
frameSelector: FrameSelector,
getFrameElement: (frame: Element, abortSignal: AbortSignal) => Promise<Element>
): Promise<void>;
declare function navigateTo({
href,
httpMethod,
bodyParams,
updateHistory,
historyHref,
frameId,
}: NavigationOptions): Promise<void>;
declare const externalApi: {
readonly executeAction: typeof executeAction;
readonly loadRoute: typeof loadRoute;
readonly navigateToAction: typeof navigateToAction;
readonly navigateToRoute: typeof navigateToRoute;
readonly navigateTo: typeof navigateTo;
readonly updateFrame: typeof updateFrame;
readonly addEventListener: typeof addEventListener;
readonly submitForm: typeof submitForm;
};

@@ -97,0 +125,0 @@ declare global {

"use strict";
// source/browser/events.browser.ts
var eventRemovalSymbol = Symbol();
function addEventListener(elementId, eventName, eventListener, options) {
const element = document.getElementById(elementId);
if (!element) {
throw new Error(`Unable to find element with id '${elementId}'.`);
}
const abortController = new AbortController();
const eventOptions =
typeof options === "boolean"
? { capture: options, signal: abortController.signal }
: { ...options, signal: abortController.signal };
if (typeof options === "object" && options.signal) {
options.signal.addEventListener("abort", () => abortController.abort());
}
element.addEventListener(eventName, eventListener, eventOptions);
element[eventRemovalSymbol] ?? (element[eventRemovalSymbol] = []);
element[eventRemovalSymbol].push(abortController);
}
function removeEventListeners(element) {
element[eventRemovalSymbol]?.forEach((abortController) => abortController.abort());
}
// source/browser/log.browser.ts
var log = {
error: (error) => console.error(error),
warn: (warning) => console.warn(warning),
info: (message) => console.log(message)
error: (error) => console.error(error),
warn: (warning) => console.warn(warning),
info: (message) => console.log(message),
};

@@ -12,309 +35,413 @@

function reconcile(currentElement, newElement) {
prepareScriptElements(newElement);
return reconcileNode(currentElement, newElement);
prepareScriptElements(newElement);
return reconcileNode(currentElement, newElement);
}
function reconcileNode(currentNode, newNode) {
if (currentNode.nodeType !== newNode.nodeType || currentNode.nodeName !== newNode.nodeName) {
return newNode;
}
if (currentNode.nodeType === Node.TEXT_NODE) {
if (currentNode.nodeValue !== newNode.nodeValue) {
currentNode.nodeValue = newNode.nodeValue;
if (currentNode.nodeType !== newNode.nodeType || currentNode.nodeName !== newNode.nodeName) {
return newNode;
}
} else if (currentNode.nodeType === Node.ELEMENT_NODE) {
if (newNode.nodeName === "SCRIPT") {
return newNode;
if (currentNode.nodeType === Node.TEXT_NODE) {
if (currentNode.nodeValue !== newNode.nodeValue) {
currentNode.nodeValue = newNode.nodeValue;
}
} else if (currentNode.nodeType === Node.ELEMENT_NODE) {
if (newNode.nodeName === "SCRIPT") {
return newNode;
}
reconcileElement(currentNode, newNode);
}
reconcileElement(currentNode, newNode);
}
return currentNode;
return currentNode;
}
function reconcileElement(currentElement, newElement) {
reconcileAttributes(currentElement, newElement);
const hasChildren = currentElement.firstChild || newElement.firstChild;
if (hasChildren) {
reconcileChildren(currentElement, newElement);
}
removeEventListeners(currentElement);
reconcileAttributes(currentElement, newElement);
const hasChildren = currentElement.firstChild ?? newElement.firstChild;
if (hasChildren) {
reconcileChildren(currentElement, newElement);
}
}
function reconcileAttributes(currentElement, newElement) {
for (const newAttribute of newElement.getAttributeNames()) {
if (newAttribute.startsWith("data-hy-view-") || newAttribute === "style") {
continue;
for (const newAttribute of newElement.getAttributeNames()) {
if (newAttribute.startsWith("data-hy-view-") || newAttribute === "style") {
continue;
}
const value = newElement.getAttribute(newAttribute);
if (value && currentElement.getAttribute(newAttribute) !== value) {
currentElement.setAttribute(newAttribute, value);
}
}
const value = newElement.getAttribute(newAttribute);
if (value && currentElement.getAttribute(newAttribute) !== value) {
currentElement.setAttribute(newAttribute, value);
for (const currentAttribute of currentElement.getAttributeNames()) {
if (currentAttribute.startsWith("data-hy-view-") || currentAttribute === "style") {
continue;
}
if (!newElement.hasAttribute(currentAttribute)) {
currentElement.removeAttribute(currentAttribute);
}
}
}
for (const currentAttribute of currentElement.getAttributeNames()) {
if (currentAttribute.startsWith("data-hy-view-") || currentAttribute === "style") {
continue;
}
if (!newElement.hasAttribute(currentAttribute)) {
currentElement.removeAttribute(currentAttribute);
}
}
}
function reconcileChildren(currentElement, newElement) {
const currentKeyMap = getKeyMap(currentElement);
const newKeyMap = getKeyMap(newElement);
let currentChild = currentElement.firstChild;
let newChild = newElement.firstChild;
while (currentChild || newChild) {
if (currentChild && !newChild) {
const oldCurrent = currentChild;
currentChild = currentChild.nextSibling;
currentElement.removeChild(oldCurrent);
} else if (!currentChild && newChild) {
const addChild = newChild;
newChild = newChild.nextSibling;
currentElement.appendChild(addChild);
} else if (currentChild && newChild) {
const currentKey = currentChild.nodeType === Node.ELEMENT_NODE ? getKey(currentChild) : void 0;
const newKey = newChild.nodeType === Node.ELEMENT_NODE ? getKey(newChild) : void 0;
if (newKey === currentKey) {
const element = reconcileNode(currentChild, newChild);
const oldCurrent = currentChild;
currentChild = currentChild.nextSibling;
newChild = newChild.nextSibling;
if (oldCurrent !== element) {
currentElement.replaceChild(element, oldCurrent);
}
} else {
if (!newKey) {
const nextNewChild = newChild.nextSibling;
currentElement.insertBefore(newChild, currentChild);
newChild = nextNewChild;
} else {
const existingChild = currentKeyMap.get(newKey);
const nextNewChild = newChild.nextSibling;
if (existingChild) {
const element = reconcileNode(existingChild, newChild);
currentElement.insertBefore(element, currentChild);
if (element !== existingChild) {
existingChild.remove();
const currentKeyMap = getKeyMap(currentElement);
const newKeyMap = getKeyMap(newElement);
let currentChild = currentElement.firstChild;
let newChild = newElement.firstChild;
while (currentChild ?? newChild) {
if (currentChild && !newChild) {
const oldCurrent = currentChild;
currentChild = currentChild.nextSibling;
currentElement.removeChild(oldCurrent);
} else if (!currentChild && newChild) {
const addChild = newChild;
newChild = newChild.nextSibling;
currentElement.appendChild(addChild);
} else if (currentChild && newChild) {
const currentKey =
currentChild.nodeType === Node.ELEMENT_NODE ? getKey(currentChild) : void 0;
const newKey = newChild.nodeType === Node.ELEMENT_NODE ? getKey(newChild) : void 0;
if (newKey === currentKey) {
const element = reconcileNode(currentChild, newChild);
const oldCurrent = currentChild;
currentChild = currentChild.nextSibling;
newChild = newChild.nextSibling;
if (oldCurrent !== element) {
currentElement.replaceChild(element, oldCurrent);
}
} else {
if (!newKey) {
const nextNewChild = newChild.nextSibling;
currentElement.insertBefore(newChild, currentChild);
newChild = nextNewChild;
} else {
const existingChild = currentKeyMap.get(newKey);
const nextNewChild = newChild.nextSibling;
if (existingChild) {
const element = reconcileNode(existingChild, newChild);
currentElement.insertBefore(element, currentChild);
if (element !== existingChild) {
existingChild.remove();
}
} else {
currentElement.insertBefore(newChild, currentChild);
}
if (currentKey && !newKeyMap.get(currentKey)) {
const childToRemove = currentChild;
currentChild = currentChild.nextSibling;
childToRemove.remove();
}
newChild = nextNewChild;
}
}
} else {
currentElement.insertBefore(newChild, currentChild);
}
if (currentKey && !newKeyMap.get(currentKey)) {
const childToRemove = currentChild;
currentChild = currentChild.nextSibling;
childToRemove.remove();
}
newChild = nextNewChild;
}
}
}
}
}
function getKey(element) {
return element.getAttribute("data-hy-key");
return element.getAttribute("data-hy-key");
}
function getKeyMap(element) {
const keyMap = /* @__PURE__ */ new Map();
for (let child = element.firstChild; child; child = child.nextSibling) {
if (child.nodeType === Node.ELEMENT_NODE) {
const elementChild = child;
const key = getKey(elementChild);
if (key) {
if (keyMap.has(key)) {
console.warn(`Key '${key}' is used more than once.`);
const keyMap = /* @__PURE__ */ new Map();
for (let child = element.firstChild; child; child = child.nextSibling) {
if (child.nodeType === Node.ELEMENT_NODE) {
const elementChild = child;
const key = getKey(elementChild);
if (key) {
if (keyMap.has(key)) {
log.warn(`Key '${key}' is used more than once.`);
}
keyMap.set(key, elementChild);
}
}
keyMap.set(key, elementChild);
}
}
}
return keyMap;
return keyMap;
}
function prepareScriptElements(newElement) {
const cspNonce = document.querySelector('meta[name="hy-csp-nonce"]')?.content;
const fragment = document.createDocumentFragment();
fragment.appendChild(newElement);
fragment.querySelectorAll("script").forEach((currentScript) => {
const newScript = document.createElement("script");
for (const attribute of currentScript.getAttributeNames()) {
const value = newScript.getAttribute(attribute);
if (value) {
newScript.setAttribute(attribute, value);
}
}
newScript.textContent = currentScript.textContent;
newScript.async = false;
newScript.nonce = cspNonce;
currentScript.parentNode?.replaceChild(newScript, currentScript);
});
const cspNonce = document.querySelector('meta[name="hy-csp-nonce"]')?.content;
const fragment = document.createDocumentFragment();
fragment.appendChild(newElement);
fragment.querySelectorAll("script").forEach((currentScript) => {
const newScript = document.createElement("script");
for (const attribute of currentScript.getAttributeNames()) {
const value = newScript.getAttribute(attribute);
if (value) {
newScript.setAttribute(attribute, value);
}
}
newScript.textContent = currentScript.textContent;
newScript.async = false;
newScript.nonce = cspNonce;
currentScript.parentNode.replaceChild(newScript, currentScript);
});
}
// source/browser/frame.browser.ts
async function navigateToRoute(frameSelector, routeUrl) {
return withHistoryUpdate(
frameSelector,
routeUrl,
(frame, signal) => fetchRoute(frame, routeUrl, signal)
);
var rootFrameId = "root";
function updateFrame(frameId, getFrameElement) {
const frame = document.getElementById(frameId);
if (!frame || frame.tagName.toLowerCase() !== "hy-frame") {
throw new Error(`Frame '${frameId}' not found.`);
}
const abortController = new AbortController();
const updateFramePromise = (async () => {
await abortPreviousUpdate(frame);
const childFrames = [...document.querySelectorAll("hy-frame")].filter(
(childFrame) => childFrame !== frame && frame.contains(childFrame),
);
const newFrame = await getFrameElement(frame, abortController.signal);
if (newFrame) {
abortController.signal.throwIfAborted();
reconcile(frame, newFrame);
await Promise.all(childFrames.map(abortPreviousUpdate));
}
})();
pendingUpdates.set(frame, [abortController, updateFramePromise]);
return updateFramePromise;
}
function navigateToAction(frameSelector, actionUrl, actionParams, routeUrlForHistory) {
return withHistoryUpdate(
frameSelector,
routeUrlForHistory,
(frame, signal) => fetchAction(frame, actionUrl, actionParams, signal)
);
var pendingUpdates = /* @__PURE__ */ new WeakMap();
async function abortPreviousUpdate(frame) {
const [previousSignal, previousPromise] = pendingUpdates.get(frame) ?? [];
previousSignal?.abort();
try {
await previousPromise;
} catch (e) {
if (e instanceof DOMException && e.name === "AbortError") {
} else {
log.warn(`An error occurred in an aborted frame update: ${e}`);
}
}
}
async function withHistoryUpdate(frameSelector, historyUrl, fetch2) {
const thisNavigationId = ++navigationId;
history.pushState({ [navigationIdKey]: thisNavigationId }, "", historyUrl);
let response = void 0;
await updateFrame(frameSelector, async (frame, signal) => {
response = await fetch2(frame, signal);
return extractFrameFromResponse(frame, response);
});
if (response.redirected && history.state[navigationIdKey] === thisNavigationId) {
history.replaceState(null, "", response.url);
}
async function fetchFrame(frame, url, fetchOptions) {
try {
return await fetch(url, {
...fetchOptions,
headers: {
...fetchOptions.headers,
// see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers
"x-hy": "true",
"x-hy-frame-id": frame.getAttribute("id") ?? "error: unknown frame id",
...(!fetchOptions.method || fetchOptions.method === "GET"
? {}
: { "content-type": "application/x-www-form-urlencoded" }),
},
});
} catch (e) {
if (isNetworkError(e)) {
frame.dispatchEvent(new CustomEvent("hy:offline", { bubbles: true, cancelable: true }));
}
throw e;
}
}
function loadRoute(frameSelector, routeUrl) {
return updateFrame(
frameSelector,
async (frame, signal) => extractFrameFromResponse(frame, await fetchRoute(frame, routeUrl, signal))
);
function isNetworkError(e) {
return e instanceof TypeError;
}
async function executeAction(frameSelector, actionUrl, actionParams) {
return updateFrame(
frameSelector,
async (frame, signal) => extractFrameFromResponse(frame, await fetchAction(frame, actionUrl, actionParams, signal))
);
async function extractFrameFromResponse(frame, response, signal) {
const document2 = await parseHTML(response);
signal?.throwIfAborted();
const frameId = frame.getAttribute("id") ?? "";
const newFrameElement = document2.getElementById(frameId);
if (!newFrameElement || newFrameElement.tagName.toLowerCase() !== "hy-frame") {
const updateFrameForErrorResponses = frame.dispatchEvent(
new CustomEvent("hy:frame-missing", {
bubbles: true,
cancelable: true,
detail: { response, document: document2 },
}),
);
const message = `Frame '${frameId}' not found in the server's response.`;
if (updateFrameForErrorResponses && response.status >= 300) {
log.warn(message);
const newFrame = frame.cloneNode();
newFrame.replaceChildren(...document2.body.children);
return newFrame;
} else {
throw new Error(message);
}
}
return newFrameElement;
}
async function updateFrame(frameSelector, getFrameElement) {
const frame = document.querySelector(frameSelector);
if (!frame) {
throw new Error(`Frame '${frameSelector}' not found.`);
}
await abortPreviousUpdate(frame);
const childFrames = [...document.querySelectorAll("hy-frame")].filter(
(frame2) => frame2 !== frame2 && frame2.contains(frame2)
);
const abortController = new AbortController();
const newFramePromise = getFrameElement(frame, abortController.signal);
pendingUpdates.set(frame, [abortController, newFramePromise]);
reconcile(frame, await newFramePromise);
await Promise.all(childFrames.map(abortPreviousUpdate));
}
var parseHTML = (() => {
const parser = new DOMParser();
return async (response) => {
const contentType = response.headers.get("content-type") ?? "<unknown>";
if (!contentType.startsWith("text/html")) {
throw new Error(`Expected content-type 'text/html' instead of '${contentType}'.`);
}
return parser.parseFromString(await response.text(), "text/html");
};
const parser = new DOMParser();
return async (response) => {
const contentType = response.headers.get("content-type") ?? "<unknown>";
if (!contentType.startsWith("text/html")) {
throw new Error(`Expected content-type 'text/html' instead of '${contentType}'.`);
}
return parser.parseFromString(await response.text(), "text/html");
};
})();
var pendingUpdates = /* @__PURE__ */ new WeakMap();
async function abortPreviousUpdate(frame) {
const [previousSignal, previousPromise] = pendingUpdates.get(frame) ?? [];
previousSignal?.abort();
try {
await previousPromise;
} catch (e) {
if (e instanceof DOMException && e.name === "AbortError") {
} else {
log.warn(`An error occurred in an aborted frame update: ${e}`);
// source/browser/form.browser.ts
async function submitForm({ href, form, additionalData, frameId, updateHistory }) {
let response = void 0;
let submissionSuccessful = false;
const formFrameId = getFormFrameId(form);
await updateFrame(formFrameId, async (frame, signal) => {
response = await fetchFrame(frame, href, {
method: "POST",
body: createFormRequestBody(form, additionalData),
signal,
});
submissionSuccessful = response.status < 300;
return submissionSuccessful
? void 0
: await extractFrameFromResponse(frame, response, signal);
});
if (submissionSuccessful) {
frameId ??
(frameId =
document
.getElementById(formFrameId)
?.parentElement?.closest("hy-frame")
?.getAttribute("id") ?? rootFrameId);
await updateFrame(frameId, (frame, signal) =>
extractFrameFromResponse(frame, response, signal),
);
if (updateHistory ?? frameId === rootFrameId) {
history.pushState(null, "", response.redirected ? response.url : href);
}
}
}
}
function fetchRoute(frame, routeUrl, signal) {
return fetchFrame(frame, routeUrl, {
signal,
headers: { "x-hytts": "true" }
});
async function validateForm({ href, form }) {
await updateFrame(getFormFrameId(form), async (frame, signal) =>
extractFrameFromResponse(
frame,
await fetchFrame(frame, href, {
method: "POST",
body: createFormRequestBody(form),
signal,
headers: { "x-hy-validate-form": "true" },
}),
signal,
),
);
}
function fetchAction(frame, actionUrl, actionParams, signal) {
return fetchFrame(frame, actionUrl, {
method: "post",
body: actionParams,
signal,
headers: {
"content-type": "application/x-www-form-urlencoded",
// see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers
"x-hytts": "true"
function interceptForms() {
document.addEventListener("submit", (e) => {
const [form, options] = getForm(e);
if (!form || !options) {
return;
}
e.preventDefault();
void submitForm({
href: form.action,
form,
frameId: options.hyFrame,
});
});
document.addEventListener("change", (e) => {
const [form, options] = getForm(e);
if (!form || !options?.hyValidate) {
return;
}
e.preventDefault();
void validateForm({
href: options.hyValidate,
form,
});
});
function getForm(e) {
const form =
e.target instanceof HTMLFormElement
? e.target
: e.target && "form" in e.target && e.target.form instanceof HTMLFormElement
? e.target.form
: void 0;
if (e.defaultPrevented || !form) {
return [void 0, void 0];
}
const options = form.dataset;
if (form.method.toUpperCase() !== "POST" || !form.action || !options?.hyValidate) {
return [void 0, void 0];
}
return [form, options];
}
});
}
async function fetchFrame(frame, url, fetchOptions) {
try {
return await fetch(url, fetchOptions);
} catch (e) {
if (e instanceof TypeError) {
frame.dispatchEvent(new Event("hy:offline", { bubbles: true }));
function createFormRequestBody(form, additionalData) {
const params = new URLSearchParams();
for (const [name, value] of new FormData(form)) {
params.append("$form." + name, value);
}
throw e;
}
return params.toString() + (additionalData ? `"&${additionalData}` : "");
}
async function extractFrameFromResponse(frame, response) {
const document2 = await parseHTML(response);
const selector = frame instanceof HTMLBodyElement ? "body" : `#${frame.id}`;
const newFrameElement = document2.querySelector(selector);
if (!newFrameElement) {
const updateFrameForErrorResponses = frame.dispatchEvent(
new CustomEvent("hy:frame-missing", {
bubbles: true,
cancelable: true,
detail: { response, newFrameElement }
})
);
if (updateFrameForErrorResponses && response.status >= 300) {
const newFrame = frame.cloneNode();
newFrame.replaceChildren(...document2.body.children);
return newFrame;
} else {
throw new Error(`Frame '${selector}' not found in the server's response.`);
}
}
return newFrameElement;
function getFormFrameId(form) {
return `${form.getAttribute("id")}@frame`;
}
var navigationId = 0;
var navigationIdKey = "hyNavigationId";
// source/browser/navigation.browser.ts
async function navigateTo({ href, httpMethod, bodyParams, updateHistory, historyHref, frameId }) {
updateHistory ?? (updateHistory = frameId === rootFrameId);
let response = void 0;
await updateFrame(frameId, async (frame, signal) => {
response = await fetchFrame(frame, href, {
method: httpMethod,
body: httpMethod === "GET" ? void 0 : bodyParams,
signal,
});
return await extractFrameFromResponse(frame, response, signal);
});
if (updateHistory) {
history.pushState(null, "", historyHref ?? (response.redirected ? response.url : href));
}
}
function interceptClicks() {
window.onclick = (e) => {
const target = e.target?.closest("a") ?? e.target?.closest("button");
const options = target?.dataset;
if (!target || !options || !options.hyNavigate) {
return;
}
e.preventDefault();
const navigationKind = options.hyNavigate;
const href = (target instanceof HTMLAnchorElement ? target.href : void 0) ?? options.hyUrl;
if (!href || !options.hyFrame) {
throw new Error("Unknown navigation URL or target frame.");
}
switch (navigationKind) {
case "route":
(options.hyUpdateHistory ?? options.hyFrame === "body" ? navigateToRoute : loadRoute)(options.hyFrame, href);
break;
case "action":
if (options.hyHistoryUrl) {
navigateToAction(
options.hyFrame,
href,
options.hyActionParams ?? "",
options.hyHistoryUrl
);
} else {
executeAction(options.hyFrame, href, options.hyActionParams ?? "");
document.addEventListener("click", (e) => {
if (e.defaultPrevented) {
return;
}
break;
case void 0:
break;
default: {
const switchGuard = navigationKind;
throw new Error(`Unknown 'data-hy-navigate' value '${navigationKind}'.`);
}
}
};
const target = e.target.closest("a") ?? e.target.closest("button");
const options = target?.dataset;
if (!target || !options?.hyMethod) {
return;
}
e.preventDefault();
const method = options.hyMethod;
const href = (target instanceof HTMLAnchorElement ? target.href : void 0) ?? options.hyUrl;
if (!href) {
throw new Error("Unknown navigation URL.");
}
if (!options.hyFrame) {
throw new Error("Unknown target frame.");
}
switch (method) {
case "GET":
void navigateTo({
frameId: options.hyFrame,
href,
httpMethod: "GET",
updateHistory:
// We have to explicitly pass through the `undefined` so that the root frame
// logic kicks in.
options.hyUpdateHistory === void 0 ? void 0 : !!options.hyUpdateHistory,
});
break;
case "POST": {
let bodyParams = options.hyBody;
if (options.hyForm) {
const form = document.getElementById(options.hyForm);
if (!form || !(form instanceof HTMLFormElement)) {
throw new Error(`Unable to find form '${options.hyForm}'.`);
}
bodyParams = createFormRequestBody(form, bodyParams);
}
void navigateTo({
frameId: options.hyFrame,
href,
httpMethod: "POST",
bodyParams,
updateHistory: !!options.hyUpdateHistory,
historyHref: options.hyUpdateHistory,
});
break;
}
case void 0:
break;
default: {
const switchGuard = method;
throw new Error(`Unknown or unsupported HTTP request method '${method}'.`);
}
}
});
}
function interceptHistoryChanges() {
window.onpopstate = () => loadRoute("body", location.href);
window.addEventListener(
"popstate",
() => void navigateTo({ frameId: rootFrameId, href: location.href, httpMethod: "GET" }),
);
}

@@ -324,10 +451,10 @@

interceptClicks();
interceptForms();
interceptHistoryChanges();
var externalApi = {
executeAction,
loadRoute,
navigateToAction,
navigateToRoute,
updateFrame
navigateTo,
updateFrame,
addEventListener,
submitForm,
};
window.hy = externalApi;
{
"name": "@hytts/hytts",
"version": "0.1.0",
"version": "0.2.3",
"author": "Axel Habermaier",
"license": "MIT",
"homepage": "https://github.com/HyTTS/HyTTS",
"description": "HyTTS (pronounced 'heights') is a framework for server-side rendered web apps based on TypeScript. ",
"description": "HyTTS (pronounced \"heights\") is a JSX-based full-stack framework with end-to-end type safety for server-side rendered web apps, inspired by Turbo and htmx. ",
"keywords": [

@@ -12,3 +12,5 @@ "TypeScript",

"server-side rendering",
"JSX"
"JSX",
"type safety",
"type-safe routing"
],

@@ -28,6 +30,5 @@ "repository": {

"@js-joda/core": "5.5.3",
"express": "4.18.2",
"lodash": "4.17.21",
"qs": "6.11.2",
"zod": "3.21.4"
"zod": "3.22.3"
},

@@ -34,0 +35,0 @@ "engines": {

# HyperText TypeScript (HyTTS)
HyTTS (pronounced "heights") is a framework for server-side rendered web apps based on TypeScript.
It is inspired by [React](https://reactjs.org/) and its component-oriented, declarative nature, but uses server-side rendering instead, with SPA-like interactivity based on concepts found in [Turbo](https://hotwired.dev/) and [htmx](https://htmx.org/).
HyTTS (pronounced "heights") is a full-stack web framework for server-side rendered web apps written in TypeScript.
End-to-end type safety from server code to browser code and back is one of its major design principles.
While HyTTS is heavily inspired by [React](https://reactjs.org/) and its JSX-based, component-oriented and declarative nature, it exclusively uses server-side rendering instead, with SPA-like interactivity based on concepts found in [Turbo](https://hotwired.dev/) and [htmx](https://htmx.org/).

@@ -10,3 +11,5 @@ HyTTS' goal is to reduce the complexity of modern-day web development while retaining the user and developer experience improvements achieved by the web development community in recent years.

HyTTS is currently under active development.
Things might change considerably in an effort to enhance the features set and to reduce the complexity of HyTTS' API and implementation to a minimum.
HyTTS is currently under active development after having completed a successful experimentation and prototyping phase.
In the coming months, things might change considerably in an effort to enhance the feature set and to reduce the complexity of HyTTS' API and implementation.
Thus, HyTTS is not yet ready for production use and does not have any documentation yet.

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc