tachyon-dom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +339 -0
- package/dist/adapters/lambda.d.ts +65 -0
- package/dist/adapters/lambda.js +211 -0
- package/dist/adapters/node.d.ts +25 -0
- package/dist/adapters/node.js +360 -0
- package/dist/adapters/workers.d.ts +149 -0
- package/dist/adapters/workers.js +272 -0
- package/dist/adapters.d.ts +3 -0
- package/dist/adapters.js +3 -0
- package/dist/app.d.ts +98 -0
- package/dist/app.js +306 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +711 -0
- package/dist/compiler/expression.d.ts +55 -0
- package/dist/compiler/expression.js +858 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +50 -0
- package/dist/compiler/ir.d.ts +4 -0
- package/dist/compiler/ir.js +255 -0
- package/dist/compiler/parser.d.ts +3 -0
- package/dist/compiler/parser.js +236 -0
- package/dist/compiler/sfc.d.ts +33 -0
- package/dist/compiler/sfc.js +439 -0
- package/dist/compiler/targets/client.d.ts +3 -0
- package/dist/compiler/targets/client.js +568 -0
- package/dist/compiler/targets/server.d.ts +7 -0
- package/dist/compiler/targets/server.js +396 -0
- package/dist/compiler/targets/stream.d.ts +2 -0
- package/dist/compiler/targets/stream.js +204 -0
- package/dist/compiler/types.d.ts +185 -0
- package/dist/compiler/types.js +1 -0
- package/dist/compiler/utils.d.ts +40 -0
- package/dist/compiler/utils.js +185 -0
- package/dist/compiler/whitespace.d.ts +2 -0
- package/dist/compiler/whitespace.js +66 -0
- package/dist/compiler.d.ts +1 -0
- package/dist/compiler.js +1 -0
- package/dist/constant-time.d.ts +1 -0
- package/dist/constant-time.js +26 -0
- package/dist/cookies.d.ts +44 -0
- package/dist/cookies.js +199 -0
- package/dist/diagnostics.d.ts +22 -0
- package/dist/diagnostics.js +45 -0
- package/dist/env.d.ts +27 -0
- package/dist/env.js +37 -0
- package/dist/html-escape.d.ts +2 -0
- package/dist/html-escape.js +20 -0
- package/dist/html-whitespace.d.ts +6 -0
- package/dist/html-whitespace.js +83 -0
- package/dist/i18n.d.ts +19 -0
- package/dist/i18n.js +93 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +18 -0
- package/dist/language-server.d.ts +17 -0
- package/dist/language-server.js +79 -0
- package/dist/package-integrity.d.ts +9 -0
- package/dist/package-integrity.js +64 -0
- package/dist/result.d.ts +13 -0
- package/dist/result.js +4 -0
- package/dist/router-node.d.ts +2 -0
- package/dist/router-node.js +12 -0
- package/dist/router.d.ts +324 -0
- package/dist/router.js +1024 -0
- package/dist/runtime/attr.d.ts +3 -0
- package/dist/runtime/attr.js +66 -0
- package/dist/runtime/class.d.ts +2 -0
- package/dist/runtime/class.js +10 -0
- package/dist/runtime/conditional.d.ts +78 -0
- package/dist/runtime/conditional.js +140 -0
- package/dist/runtime/enhancement.d.ts +11 -0
- package/dist/runtime/enhancement.js +70 -0
- package/dist/runtime/error-boundary.d.ts +8 -0
- package/dist/runtime/error-boundary.js +27 -0
- package/dist/runtime/event.d.ts +1 -0
- package/dist/runtime/event.js +11 -0
- package/dist/runtime/form.d.ts +41 -0
- package/dist/runtime/form.js +167 -0
- package/dist/runtime/fragment.d.ts +6 -0
- package/dist/runtime/fragment.js +18 -0
- package/dist/runtime/html.d.ts +8 -0
- package/dist/runtime/html.js +6 -0
- package/dist/runtime/hydrate.d.ts +68 -0
- package/dist/runtime/hydrate.js +205 -0
- package/dist/runtime/keyed-rows.d.ts +61 -0
- package/dist/runtime/keyed-rows.js +192 -0
- package/dist/runtime/list.d.ts +84 -0
- package/dist/runtime/list.js +424 -0
- package/dist/runtime/portal.d.ts +6 -0
- package/dist/runtime/portal.js +12 -0
- package/dist/runtime/router.d.ts +102 -0
- package/dist/runtime/router.js +649 -0
- package/dist/runtime/signal.d.ts +30 -0
- package/dist/runtime/signal.js +275 -0
- package/dist/runtime/store.d.ts +1 -0
- package/dist/runtime/store.js +1 -0
- package/dist/runtime/stream-client.d.ts +8 -0
- package/dist/runtime/stream-client.js +33 -0
- package/dist/runtime/text.d.ts +2 -0
- package/dist/runtime/text.js +10 -0
- package/dist/runtime/virtual-list.d.ts +15 -0
- package/dist/runtime/virtual-list.js +89 -0
- package/dist/security.d.ts +27 -0
- package/dist/security.js +173 -0
- package/dist/server/form-action.d.ts +52 -0
- package/dist/server/form-action.js +102 -0
- package/dist/server/html.d.ts +19 -0
- package/dist/server/html.js +101 -0
- package/dist/server/stream.d.ts +4 -0
- package/dist/server/stream.js +72 -0
- package/dist/source-map.d.ts +17 -0
- package/dist/source-map.js +18 -0
- package/dist/tachyon-html.d.ts +110 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +36 -0
- package/dist/typed.d.ts +9 -0
- package/dist/typed.js +4 -0
- package/dist/vite.d.ts +84 -0
- package/dist/vite.js +426 -0
- package/package.json +604 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { attr, type HtmlFragment } from "./html.js";
|
|
2
|
+
export type FormValue = string | string[];
|
|
3
|
+
export type FormState<Fields extends string = string> = {
|
|
4
|
+
values: Partial<Record<Fields, FormValue>>;
|
|
5
|
+
fieldErrors: Partial<Record<Fields, string>>;
|
|
6
|
+
formError?: string;
|
|
7
|
+
notice?: string;
|
|
8
|
+
};
|
|
9
|
+
export type FormActionResult<Input, ErrorState> = {
|
|
10
|
+
ok: true;
|
|
11
|
+
value: Input;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
error: ErrorState;
|
|
15
|
+
};
|
|
16
|
+
export type FormActionContext = {
|
|
17
|
+
request: Request;
|
|
18
|
+
formData: FormData;
|
|
19
|
+
};
|
|
20
|
+
export type FormActionOptions<Input, ErrorState> = {
|
|
21
|
+
parse: (formData: FormData, context: FormActionContext) => FormActionResult<Input, ErrorState> | Promise<FormActionResult<Input, ErrorState>>;
|
|
22
|
+
onSuccess: (context: FormActionContext & {
|
|
23
|
+
input: Input;
|
|
24
|
+
}) => Response | Promise<Response>;
|
|
25
|
+
onError: (context: FormActionContext & {
|
|
26
|
+
error: ErrorState;
|
|
27
|
+
}) => Response | Promise<Response>;
|
|
28
|
+
};
|
|
29
|
+
export type PreserveFormValuesOptions = {
|
|
30
|
+
sensitiveNames?: readonly string[];
|
|
31
|
+
include?: readonly string[];
|
|
32
|
+
exclude?: readonly string[];
|
|
33
|
+
};
|
|
34
|
+
export type RedirectResponseOptions = ResponseInit & {
|
|
35
|
+
allowExternal?: boolean;
|
|
36
|
+
allowedOrigins?: readonly string[];
|
|
37
|
+
};
|
|
38
|
+
export type FormFieldOptions = {
|
|
39
|
+
id?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
errorId?: string;
|
|
42
|
+
};
|
|
43
|
+
export declare const formState: <Fields extends string = string>(state?: Partial<FormState<Fields>>) => FormState<Fields>;
|
|
44
|
+
export declare const preserveFormValues: (formData: FormData, options?: PreserveFormValuesOptions) => Record<string, FormValue>;
|
|
45
|
+
export declare const redirectResponse: (location: string, init?: RedirectResponseOptions) => Response;
|
|
46
|
+
export declare const formAction: <Input, ErrorState>({ parse, onSuccess, onError, }: FormActionOptions<Input, ErrorState>) => ((request: Request) => Promise<Response>);
|
|
47
|
+
export declare const formField: <Fields extends string>(state: FormState<Fields>, fieldName: Fields, options?: FormFieldOptions) => {
|
|
48
|
+
inputAttrs: () => ReturnType<typeof attr>[];
|
|
49
|
+
error: () => HtmlFragment;
|
|
50
|
+
errorId: string;
|
|
51
|
+
value: FormValue | undefined;
|
|
52
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { attr, html } from "./html.js";
|
|
2
|
+
const defaultSensitiveNames = ["password", "currentPassword", "newPassword", "confirmPassword", "passwordConfirmation"];
|
|
3
|
+
const stringValue = (value) => typeof File !== "undefined" && value instanceof File ? value.name : String(value);
|
|
4
|
+
const fieldIdFor = (name) => name.replaceAll(/[^A-Za-z0-9_-]+/g, "-").replaceAll(/^-|-$/g, "") || "field";
|
|
5
|
+
const appendValue = (values, name, value) => {
|
|
6
|
+
const current = values[name];
|
|
7
|
+
if (current === undefined) {
|
|
8
|
+
values[name] = value;
|
|
9
|
+
}
|
|
10
|
+
else if (Array.isArray(current)) {
|
|
11
|
+
current.push(value);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
values[name] = [current, value];
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export const formState = (state = {}) => ({
|
|
18
|
+
values: state.values ?? {},
|
|
19
|
+
fieldErrors: state.fieldErrors ?? {},
|
|
20
|
+
...(state.formError !== undefined ? { formError: state.formError } : {}),
|
|
21
|
+
...(state.notice !== undefined ? { notice: state.notice } : {}),
|
|
22
|
+
});
|
|
23
|
+
export const preserveFormValues = (formData, options = {}) => {
|
|
24
|
+
const sensitiveNames = new Set(options.sensitiveNames ?? defaultSensitiveNames);
|
|
25
|
+
const include = options.include ? new Set(options.include) : undefined;
|
|
26
|
+
const exclude = new Set(options.exclude ?? []);
|
|
27
|
+
const values = {};
|
|
28
|
+
for (const [name, value] of formData) {
|
|
29
|
+
if (sensitiveNames.has(name) || exclude.has(name) || (include && !include.has(name))) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
appendValue(values, name, stringValue(value));
|
|
33
|
+
}
|
|
34
|
+
return values;
|
|
35
|
+
};
|
|
36
|
+
const isSafePathRedirect = (location) => {
|
|
37
|
+
const controlCharacterPattern = /[\u0000-\u001F\u007F]/;
|
|
38
|
+
if (!location.startsWith("/") || location.startsWith("//") || controlCharacterPattern.test(location)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const decoded = decodeURIComponent(location);
|
|
43
|
+
return !decoded.startsWith("//") && !decoded.includes("\\") && !controlCharacterPattern.test(decoded);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const isApprovedExternalRedirect = (location, allowedOrigins) => {
|
|
50
|
+
if (!allowedOrigins || allowedOrigins.length === 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(location);
|
|
55
|
+
return (url.protocol === "https:" || url.protocol === "http:") && allowedOrigins.includes(url.origin);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
export const redirectResponse = (location, init = {}) => {
|
|
62
|
+
if (!isSafePathRedirect(location) &&
|
|
63
|
+
!(init.allowExternal && isApprovedExternalRedirect(location, init.allowedOrigins))) {
|
|
64
|
+
throw new Error(`Unsafe redirect target: ${location}`);
|
|
65
|
+
}
|
|
66
|
+
const headers = new Headers(init.headers);
|
|
67
|
+
headers.set("location", location);
|
|
68
|
+
return new Response(null, { ...init, status: init.status ?? 303, headers });
|
|
69
|
+
};
|
|
70
|
+
export const formAction = ({ parse, onSuccess, onError, }) => {
|
|
71
|
+
return async (request) => {
|
|
72
|
+
const formData = await request.formData();
|
|
73
|
+
const context = { request, formData };
|
|
74
|
+
const result = await parse(formData, context);
|
|
75
|
+
if (result.ok) {
|
|
76
|
+
return onSuccess({ ...context, input: result.value });
|
|
77
|
+
}
|
|
78
|
+
return onError({ ...context, error: result.error });
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export const formField = (state, fieldName, options = {}) => {
|
|
82
|
+
const name = options.name ?? fieldName;
|
|
83
|
+
const id = options.id ?? fieldIdFor(name);
|
|
84
|
+
const error = state.fieldErrors[fieldName];
|
|
85
|
+
const errorId = options.errorId ?? `${id}-error`;
|
|
86
|
+
const value = state.values[fieldName];
|
|
87
|
+
return {
|
|
88
|
+
errorId,
|
|
89
|
+
value,
|
|
90
|
+
inputAttrs: () => {
|
|
91
|
+
const firstValue = Array.isArray(value) ? (value[0] ?? "") : (value ?? "");
|
|
92
|
+
return [
|
|
93
|
+
attr("name", name),
|
|
94
|
+
attr("id", id),
|
|
95
|
+
attr("value", firstValue),
|
|
96
|
+
attr("aria-invalid", error ? "true" : undefined),
|
|
97
|
+
attr("aria-describedby", error ? errorId : undefined),
|
|
98
|
+
];
|
|
99
|
+
},
|
|
100
|
+
error: () => (error ? html `<p${attr("id", errorId)} role="alert">${error}</p>` : html ``),
|
|
101
|
+
};
|
|
102
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
declare const htmlFragmentBrand: unique symbol;
|
|
2
|
+
declare const htmlAttributeBrand: unique symbol;
|
|
3
|
+
export type HtmlFragment = {
|
|
4
|
+
readonly [htmlFragmentBrand]: true;
|
|
5
|
+
toString(): string;
|
|
6
|
+
};
|
|
7
|
+
export type HtmlAttribute = {
|
|
8
|
+
readonly [htmlAttributeBrand]: true;
|
|
9
|
+
toString(): string;
|
|
10
|
+
};
|
|
11
|
+
type HtmlValue = HtmlFragment | HtmlAttribute | readonly HtmlValue[] | string | number | boolean | null | undefined;
|
|
12
|
+
type ClassValue = string | number | false | null | undefined | readonly ClassValue[];
|
|
13
|
+
export declare const rawHtml: (value: string) => HtmlFragment;
|
|
14
|
+
export declare const join: (values: readonly HtmlValue[], separator?: string) => HtmlFragment;
|
|
15
|
+
export declare const attr: (name: string, value: unknown) => HtmlAttribute;
|
|
16
|
+
export declare const booleanAttr: (name: string, enabled: boolean | null | undefined) => HtmlAttribute;
|
|
17
|
+
export declare const classList: (...values: readonly ClassValue[]) => string;
|
|
18
|
+
export declare const html: (strings: TemplateStringsArray, ...values: readonly HtmlValue[]) => HtmlFragment;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { escapeHtml as escapeText } from "../html-escape.js";
|
|
2
|
+
const htmlFragmentBrand = Symbol("tachyon.htmlFragment");
|
|
3
|
+
const htmlAttributeBrand = Symbol("tachyon.htmlAttribute");
|
|
4
|
+
const attributeNamePattern = /^[A-Za-z_:][A-Za-z0-9_.:-]*$/;
|
|
5
|
+
const attributeContextPattern = /(?:^|[\s<])(?:[A-Za-z_:][A-Za-z0-9_.:-]*)(?:\s*)=(?:\s*)$/;
|
|
6
|
+
const fragment = (value) => ({
|
|
7
|
+
[htmlFragmentBrand]: true,
|
|
8
|
+
toString: () => value,
|
|
9
|
+
});
|
|
10
|
+
const attributeFragment = (value) => ({
|
|
11
|
+
[htmlAttributeBrand]: true,
|
|
12
|
+
toString: () => value,
|
|
13
|
+
});
|
|
14
|
+
const isHtmlFragment = (value) => Boolean(value && typeof value === "object" && value[htmlFragmentBrand] === true);
|
|
15
|
+
const isHtmlAttribute = (value) => Boolean(value && typeof value === "object" && value[htmlAttributeBrand] === true);
|
|
16
|
+
const attributeEscapeMap = {
|
|
17
|
+
"&": "&",
|
|
18
|
+
"<": "<",
|
|
19
|
+
">": ">",
|
|
20
|
+
'"': """,
|
|
21
|
+
"'": "'",
|
|
22
|
+
"`": "`",
|
|
23
|
+
};
|
|
24
|
+
const attributeEscapePattern = /[&<>"'`]/;
|
|
25
|
+
const attributeEscapeGlobalPattern = /[&<>"'`]/g;
|
|
26
|
+
const escapeAttribute = (value) => {
|
|
27
|
+
const text = String(value ?? "");
|
|
28
|
+
return attributeEscapePattern.test(text)
|
|
29
|
+
? text.replace(attributeEscapeGlobalPattern, (char) => attributeEscapeMap[char] ?? char)
|
|
30
|
+
: text;
|
|
31
|
+
};
|
|
32
|
+
const assertAttributeName = (name) => {
|
|
33
|
+
if (!attributeNamePattern.test(name)) {
|
|
34
|
+
throw new Error(`Invalid attribute name: ${name}`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const renderValue = (value, context) => {
|
|
38
|
+
if (value == null || value === false) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return value.map((item) => renderValue(item, context)).join("");
|
|
43
|
+
}
|
|
44
|
+
if (isHtmlAttribute(value)) {
|
|
45
|
+
return value.toString();
|
|
46
|
+
}
|
|
47
|
+
if (isHtmlFragment(value)) {
|
|
48
|
+
return value.toString();
|
|
49
|
+
}
|
|
50
|
+
if (context === "attribute") {
|
|
51
|
+
return `"${escapeAttribute(value)}"`;
|
|
52
|
+
}
|
|
53
|
+
return escapeText(value);
|
|
54
|
+
};
|
|
55
|
+
export const rawHtml = (value) => fragment(value);
|
|
56
|
+
export const join = (values, separator = "") => fragment(values.map((value) => renderValue(value, "text")).join(separator));
|
|
57
|
+
export const attr = (name, value) => {
|
|
58
|
+
assertAttributeName(name);
|
|
59
|
+
if (value == null || value === false) {
|
|
60
|
+
return attributeFragment("");
|
|
61
|
+
}
|
|
62
|
+
if (value === true) {
|
|
63
|
+
return attributeFragment(` ${name}`);
|
|
64
|
+
}
|
|
65
|
+
return attributeFragment(` ${name}="${escapeAttribute(value)}"`);
|
|
66
|
+
};
|
|
67
|
+
export const booleanAttr = (name, enabled) => {
|
|
68
|
+
assertAttributeName(name);
|
|
69
|
+
return enabled ? attributeFragment(` ${name}`) : attributeFragment("");
|
|
70
|
+
};
|
|
71
|
+
export const classList = (...values) => {
|
|
72
|
+
const classes = [];
|
|
73
|
+
const collect = (value) => {
|
|
74
|
+
if (!value) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
for (const item of value) {
|
|
79
|
+
collect(item);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
classes.push(String(value));
|
|
84
|
+
};
|
|
85
|
+
for (const value of values) {
|
|
86
|
+
collect(value);
|
|
87
|
+
}
|
|
88
|
+
return classes.join(" ");
|
|
89
|
+
};
|
|
90
|
+
export const html = (strings, ...values) => {
|
|
91
|
+
let output = "";
|
|
92
|
+
for (let index = 0; index < strings.length; index += 1) {
|
|
93
|
+
const literal = strings[index] ?? "";
|
|
94
|
+
output += literal;
|
|
95
|
+
if (index < values.length) {
|
|
96
|
+
const context = attributeContextPattern.test(literal) ? "attribute" : "text";
|
|
97
|
+
output += renderValue(values[index], context);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return fragment(output);
|
|
101
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type HtmlChunk = string | Uint8Array;
|
|
2
|
+
export type HtmlChunkSource = Iterable<HtmlChunk | Promise<HtmlChunk>> | AsyncIterable<HtmlChunk>;
|
|
3
|
+
export declare const renderToReadableStream: (chunks: HtmlChunkSource) => ReadableStream<Uint8Array>;
|
|
4
|
+
export declare const renderToResponse: (chunks: HtmlChunkSource, init?: ResponseInit) => Response;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
const coalescedChunkTargetBytes = 8192;
|
|
3
|
+
const isAsyncIterable = (value) => Symbol.asyncIterator in value;
|
|
4
|
+
const toBytes = (chunk) => (typeof chunk === "string" ? encoder.encode(chunk) : chunk);
|
|
5
|
+
async function* toAsyncChunks(source) {
|
|
6
|
+
if (isAsyncIterable(source)) {
|
|
7
|
+
yield* source;
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
let textBuffer = "";
|
|
11
|
+
let textBufferBytes = 0;
|
|
12
|
+
const flush = function* () {
|
|
13
|
+
if (textBufferBytes > 0) {
|
|
14
|
+
yield textBuffer;
|
|
15
|
+
textBuffer = "";
|
|
16
|
+
textBufferBytes = 0;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
for (const chunk of source) {
|
|
20
|
+
const resolved = await chunk;
|
|
21
|
+
if (typeof resolved !== "string") {
|
|
22
|
+
yield* flush();
|
|
23
|
+
yield resolved;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (resolved.length === 0) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
textBuffer += resolved;
|
|
30
|
+
textBufferBytes += toBytes(resolved).byteLength;
|
|
31
|
+
if (textBufferBytes >= coalescedChunkTargetBytes) {
|
|
32
|
+
yield* flush();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
yield* flush();
|
|
36
|
+
}
|
|
37
|
+
export const renderToReadableStream = (chunks) => {
|
|
38
|
+
const iterator = toAsyncChunks(chunks)[Symbol.asyncIterator]();
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
return new ReadableStream({
|
|
41
|
+
async pull(controller) {
|
|
42
|
+
if (cancelled) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const result = await iterator.next();
|
|
47
|
+
if (result.done) {
|
|
48
|
+
controller.close();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const bytes = toBytes(result.value);
|
|
52
|
+
if (bytes.byteLength > 0) {
|
|
53
|
+
controller.enqueue(bytes);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
controller.error(error);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async cancel() {
|
|
61
|
+
cancelled = true;
|
|
62
|
+
await iterator.return?.();
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
export const renderToResponse = (chunks, init = {}) => {
|
|
67
|
+
const headers = new Headers(init.headers);
|
|
68
|
+
if (!headers.has("content-type")) {
|
|
69
|
+
headers.set("content-type", "text/html; charset=utf-8");
|
|
70
|
+
}
|
|
71
|
+
return new Response(renderToReadableStream(chunks), { ...init, headers });
|
|
72
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type SourceMap = {
|
|
2
|
+
version: 3;
|
|
3
|
+
file?: string;
|
|
4
|
+
sources: string[];
|
|
5
|
+
sourcesContent: string[];
|
|
6
|
+
names: string[];
|
|
7
|
+
mappings: string;
|
|
8
|
+
};
|
|
9
|
+
export type SourceMapBuildContext = {
|
|
10
|
+
sourcemap?: boolean | undefined;
|
|
11
|
+
productionSourceMap?: boolean | undefined;
|
|
12
|
+
command?: string | undefined;
|
|
13
|
+
mode?: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
export declare const createSourceMap: (source: string, sourceFile?: string, outputFile?: string) => SourceMap;
|
|
16
|
+
export declare const shouldEmitSourceMap: (context?: SourceMapBuildContext) => boolean;
|
|
17
|
+
export declare const appendInlineSourceMap: (code: string, map: SourceMap) => string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const createSourceMap = (source, sourceFile = "template.tachyon.html", outputFile) => ({
|
|
2
|
+
version: 3,
|
|
3
|
+
...(outputFile ? { file: outputFile } : {}),
|
|
4
|
+
sources: [sourceFile],
|
|
5
|
+
sourcesContent: [source],
|
|
6
|
+
names: [],
|
|
7
|
+
mappings: "",
|
|
8
|
+
});
|
|
9
|
+
export const shouldEmitSourceMap = (context = {}) => {
|
|
10
|
+
if (context.productionSourceMap === false && context.command === "build" && context.mode === "production") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return context.sourcemap !== false;
|
|
14
|
+
};
|
|
15
|
+
export const appendInlineSourceMap = (code, map) => {
|
|
16
|
+
const encoded = Buffer.from(JSON.stringify(map), "utf8").toString("base64");
|
|
17
|
+
return `${code}\n//# sourceMappingURL=data:application/json;base64,${encoded}\n`;
|
|
18
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
declare module "*.td" {
|
|
2
|
+
const defaultScope: Record<string, unknown> | ((scope: Record<string, unknown>) => Record<string, unknown>);
|
|
3
|
+
export default defaultScope;
|
|
4
|
+
export const scope: Record<string, unknown> | ((scope: Record<string, unknown>) => Record<string, unknown>);
|
|
5
|
+
export const templateHtml: string;
|
|
6
|
+
export const hydrationBoundaries: unknown[];
|
|
7
|
+
export const componentBoundaries: unknown[];
|
|
8
|
+
export const bind: (root: Element, scope: Record<string, unknown>) => void | (() => void);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module "*.td?raw" {
|
|
12
|
+
const source: string;
|
|
13
|
+
export default source;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module "*.td?client" {
|
|
17
|
+
export const templateHtml: string;
|
|
18
|
+
export const hydrationBoundaries: unknown[];
|
|
19
|
+
export const componentBoundaries: unknown[];
|
|
20
|
+
export const bind: (root: Element, scope: Record<string, unknown>) => void | (() => void);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare module "*.td?server" {
|
|
24
|
+
export const hydrationBoundaries: unknown[];
|
|
25
|
+
export const renderHydrationState: (id: string, state: unknown) => string;
|
|
26
|
+
export const render: (scope: Record<string, unknown>) => string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
declare module "*.td?stream" {
|
|
30
|
+
export const stream: (scope: Record<string, unknown>) => AsyncIterable<string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare module "*.td?entry" {
|
|
34
|
+
const module: Record<string, unknown>;
|
|
35
|
+
export default module;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare module "*.tachyon" {
|
|
39
|
+
const defaultScope: Record<string, unknown> | ((scope: Record<string, unknown>) => Record<string, unknown>);
|
|
40
|
+
export default defaultScope;
|
|
41
|
+
export const scope: Record<string, unknown> | ((scope: Record<string, unknown>) => Record<string, unknown>);
|
|
42
|
+
export const templateHtml: string;
|
|
43
|
+
export const hydrationBoundaries: unknown[];
|
|
44
|
+
export const componentBoundaries: unknown[];
|
|
45
|
+
export const bind: (root: Element, scope: Record<string, unknown>) => void | (() => void);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
declare module "*.tachyon?raw" {
|
|
49
|
+
const source: string;
|
|
50
|
+
export default source;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
declare module "*.tachyon?client" {
|
|
54
|
+
export const templateHtml: string;
|
|
55
|
+
export const hydrationBoundaries: unknown[];
|
|
56
|
+
export const componentBoundaries: unknown[];
|
|
57
|
+
export const bind: (root: Element, scope: Record<string, unknown>) => void | (() => void);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declare module "*.tachyon?server" {
|
|
61
|
+
export const hydrationBoundaries: unknown[];
|
|
62
|
+
export const renderHydrationState: (id: string, state: unknown) => string;
|
|
63
|
+
export const render: (scope: Record<string, unknown>) => string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
declare module "*.tachyon?stream" {
|
|
67
|
+
export const stream: (scope: Record<string, unknown>) => AsyncIterable<string>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
declare module "*.tachyon?entry" {
|
|
71
|
+
const module: Record<string, unknown>;
|
|
72
|
+
export default module;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
declare module "*.tachyon.html" {
|
|
76
|
+
const defaultScope: Record<string, unknown> | ((scope: Record<string, unknown>) => Record<string, unknown>);
|
|
77
|
+
export default defaultScope;
|
|
78
|
+
export const scope: Record<string, unknown> | ((scope: Record<string, unknown>) => Record<string, unknown>);
|
|
79
|
+
export const templateHtml: string;
|
|
80
|
+
export const hydrationBoundaries: unknown[];
|
|
81
|
+
export const componentBoundaries: unknown[];
|
|
82
|
+
export const bind: (root: Element, scope: Record<string, unknown>) => void | (() => void);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
declare module "*.tachyon.html?raw" {
|
|
86
|
+
const source: string;
|
|
87
|
+
export default source;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
declare module "*.tachyon.html?client" {
|
|
91
|
+
export const templateHtml: string;
|
|
92
|
+
export const hydrationBoundaries: unknown[];
|
|
93
|
+
export const componentBoundaries: unknown[];
|
|
94
|
+
export const bind: (root: Element, scope: Record<string, unknown>) => void | (() => void);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
declare module "*.tachyon.html?server" {
|
|
98
|
+
export const hydrationBoundaries: unknown[];
|
|
99
|
+
export const renderHydrationState: (id: string, state: unknown) => string;
|
|
100
|
+
export const render: (scope: Record<string, unknown>) => string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
declare module "*.tachyon.html?stream" {
|
|
104
|
+
export const stream: (scope: Record<string, unknown>) => AsyncIterable<string>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
declare module "*.tachyon.html?entry" {
|
|
108
|
+
const module: Record<string, unknown>;
|
|
109
|
+
export default module;
|
|
110
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TachyonApp } from "./app.js";
|
|
2
|
+
import { type RouteDefinition, type RouteRenderOptions, type RouteRenderResult } from "./router.js";
|
|
3
|
+
export declare const renderRouteForTest: (routes: readonly RouteDefinition[], url: string, options?: RouteRenderOptions) => Promise<RouteRenderResult>;
|
|
4
|
+
export type RouteParityCase = {
|
|
5
|
+
path: string;
|
|
6
|
+
clientHtml: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const assertRouteParity: (routes: readonly RouteDefinition[], cases: readonly RouteParityCase[], options?: RouteRenderOptions) => Promise<void>;
|
|
9
|
+
export declare const renderAppForTest: (app: TachyonApp, path: string) => string;
|
|
10
|
+
export declare const assertAppHtml: (app: TachyonApp, path: string, expectedFragments: readonly string[]) => void;
|
|
11
|
+
export type RenderTdForTestOptions = {
|
|
12
|
+
locale?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const renderTdForTest: (templatePath: string, scope?: Record<string, unknown>, options?: RenderTdForTestOptions) => Promise<string>;
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { diagnoseTachyonSfc, formatDiagnostic } from "./diagnostics.js";
|
|
3
|
+
import { renderServerTemplate } from "./compiler/index.js";
|
|
4
|
+
import { renderRoute } from "./router.js";
|
|
5
|
+
export const renderRouteForTest = async (routes, url, options = {}) => {
|
|
6
|
+
const result = await renderRoute(routes, new URL(url, "https://example.test"), options);
|
|
7
|
+
if (!result.ok) {
|
|
8
|
+
throw new Error(result.error.message);
|
|
9
|
+
}
|
|
10
|
+
return result.value;
|
|
11
|
+
};
|
|
12
|
+
export const assertRouteParity = async (routes, cases, options = {}) => {
|
|
13
|
+
for (const testCase of cases) {
|
|
14
|
+
const rendered = await renderRouteForTest(routes, testCase.path, options);
|
|
15
|
+
if (rendered.html !== testCase.clientHtml) {
|
|
16
|
+
throw new Error(`Route parity mismatch for ${testCase.path}: expected ${rendered.html}, received ${testCase.clientHtml}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export const renderAppForTest = (app, path) => app.renderDocument(path);
|
|
21
|
+
export const assertAppHtml = (app, path, expectedFragments) => {
|
|
22
|
+
const html = renderAppForTest(app, path);
|
|
23
|
+
const missing = expectedFragments.filter((fragment) => !html.includes(fragment));
|
|
24
|
+
if (missing.length > 0) {
|
|
25
|
+
throw new Error(`App HTML assertion failed for ${path}: missing ${missing.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export const renderTdForTest = async (templatePath, scope = {}, options = {}) => {
|
|
29
|
+
const source = await readFile(templatePath, "utf8");
|
|
30
|
+
const result = diagnoseTachyonSfc(source);
|
|
31
|
+
if (!result.ok) {
|
|
32
|
+
throw new Error(formatDiagnostic(result.error, templatePath));
|
|
33
|
+
}
|
|
34
|
+
const defaultScope = options.locale === undefined ? {} : { locale: options.locale };
|
|
35
|
+
return renderServerTemplate(result.value.template, { ...defaultScope, ...scope });
|
|
36
|
+
};
|
package/dist/typed.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type TemplateScope = Record<string, unknown>;
|
|
2
|
+
export type TypedTemplate<Scope extends TemplateScope, Source extends string = string> = {
|
|
3
|
+
source: Source;
|
|
4
|
+
__scope?: (scope: Scope) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare const defineTemplate: <Scope extends TemplateScope, const Source extends string>(source: Source) => TypedTemplate<Scope, Source>;
|
|
7
|
+
export declare const templateScope: <Scope extends TemplateScope>() => {
|
|
8
|
+
define: <const Source extends string>(source: Source) => TypedTemplate<Scope, Source>;
|
|
9
|
+
};
|
package/dist/typed.js
ADDED
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { type NodeFetchHandlerOptions, type StaticAssetOptions } from "./adapters/node.js";
|
|
4
|
+
import { type TachyonApp, type TachyonAppDefinition } from "./app.js";
|
|
5
|
+
import { type HtmlWhitespacePolicy } from "./html-whitespace.js";
|
|
6
|
+
import type { TemplateWhitespacePolicy } from "./compiler/types.js";
|
|
7
|
+
import { type Result } from "./result.js";
|
|
8
|
+
import { type SourceMap } from "./source-map.js";
|
|
9
|
+
export type TachyonDomViteOptions = {
|
|
10
|
+
include?: RegExp;
|
|
11
|
+
target?: "client" | "server" | "stream";
|
|
12
|
+
reactive?: boolean;
|
|
13
|
+
templateWhitespace?: TemplateWhitespacePolicy;
|
|
14
|
+
sourcemap?: boolean;
|
|
15
|
+
productionSourceMap?: boolean;
|
|
16
|
+
requestLog?: boolean | TachyonDomRequestLogOptions;
|
|
17
|
+
declarationOutput?: false | ((id: string) => string | undefined);
|
|
18
|
+
onSourceMap?: (artifact: {
|
|
19
|
+
id: string;
|
|
20
|
+
code: string;
|
|
21
|
+
map: SourceMap;
|
|
22
|
+
source: string;
|
|
23
|
+
}) => void | Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
export type TachyonDomRequestLogOptions = {
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
includeQuery?: boolean;
|
|
28
|
+
logger?: (message: string) => void;
|
|
29
|
+
};
|
|
30
|
+
export type TachyonDomRouteModule = {
|
|
31
|
+
id: string;
|
|
32
|
+
path: string;
|
|
33
|
+
module: string;
|
|
34
|
+
};
|
|
35
|
+
export type TachyonDomRoutesViteOptions = {
|
|
36
|
+
routes?: readonly TachyonDomRouteModule[];
|
|
37
|
+
files?: readonly string[];
|
|
38
|
+
rootDir?: string;
|
|
39
|
+
virtualId?: string;
|
|
40
|
+
};
|
|
41
|
+
export type TachyonAppViteOptions = {
|
|
42
|
+
appScript?: string;
|
|
43
|
+
htmlWhitespace?: HtmlWhitespacePolicy;
|
|
44
|
+
/** @deprecated Use `htmlWhitespace` instead. */
|
|
45
|
+
minifyHtml?: boolean;
|
|
46
|
+
};
|
|
47
|
+
export type TachyonRouteAppOptions = Omit<TachyonAppDefinition, "pages"> & {
|
|
48
|
+
routesDir: string;
|
|
49
|
+
};
|
|
50
|
+
export declare const loadRouteApp: ({ routesDir, ...definition }: TachyonRouteAppOptions) => Promise<TachyonApp>;
|
|
51
|
+
export type TachyonSsrContext = {
|
|
52
|
+
clientScript?: string;
|
|
53
|
+
};
|
|
54
|
+
export type TachyonSsrFetchHandler = (request: Request, context: TachyonSsrContext) => Response | Promise<Response>;
|
|
55
|
+
export type TachyonSsrBypass = (url: URL, request: IncomingMessage) => boolean;
|
|
56
|
+
export type TachyonSsrMiddlewareOptions = Omit<NodeFetchHandlerOptions, "fetch"> & {
|
|
57
|
+
fetch: TachyonSsrFetchHandler;
|
|
58
|
+
clientScript?: string | ((request: Request) => string | undefined | Promise<string | undefined>);
|
|
59
|
+
bypass?: TachyonSsrBypass;
|
|
60
|
+
};
|
|
61
|
+
export type TachyonSsrViteOptions = Omit<TachyonSsrMiddlewareOptions, "staticAssets"> & {
|
|
62
|
+
staticAssets?: StaticAssetOptions | false;
|
|
63
|
+
};
|
|
64
|
+
export type TachyonSsrMiddleware = (request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void) => void;
|
|
65
|
+
export type CloudflarePagesPackageOptions = {
|
|
66
|
+
entry: string;
|
|
67
|
+
outDir: string;
|
|
68
|
+
assetsDir?: string;
|
|
69
|
+
exportName?: string;
|
|
70
|
+
fallthroughStatuses?: readonly number[];
|
|
71
|
+
runtimeEnvKeys?: readonly string[];
|
|
72
|
+
};
|
|
73
|
+
export type CloudflarePagesPackageResult = {
|
|
74
|
+
outDir: string;
|
|
75
|
+
workerPath: string;
|
|
76
|
+
};
|
|
77
|
+
export declare const packageCloudflarePages: (options: CloudflarePagesPackageOptions) => Promise<Result<CloudflarePagesPackageResult, string>>;
|
|
78
|
+
export declare const tachyonDom: (options?: TachyonDomViteOptions) => Plugin;
|
|
79
|
+
export declare const tachyonDomRoutes: (options: TachyonDomRoutesViteOptions) => Plugin;
|
|
80
|
+
export declare const isViteSsrPassthroughRequest: (url: URL) => boolean;
|
|
81
|
+
export declare const createTachyonSsrMiddleware: (options: TachyonSsrMiddlewareOptions) => TachyonSsrMiddleware;
|
|
82
|
+
export declare const tachyonSsr: (options: TachyonSsrViteOptions) => Plugin;
|
|
83
|
+
export declare const tachyonApp: (app: TachyonApp, options?: TachyonAppViteOptions) => Plugin;
|
|
84
|
+
export default tachyonDom;
|