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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/dist/adapters/lambda.d.ts +65 -0
  4. package/dist/adapters/lambda.js +211 -0
  5. package/dist/adapters/node.d.ts +25 -0
  6. package/dist/adapters/node.js +360 -0
  7. package/dist/adapters/workers.d.ts +149 -0
  8. package/dist/adapters/workers.js +272 -0
  9. package/dist/adapters.d.ts +3 -0
  10. package/dist/adapters.js +3 -0
  11. package/dist/app.d.ts +98 -0
  12. package/dist/app.js +306 -0
  13. package/dist/cli.d.ts +55 -0
  14. package/dist/cli.js +711 -0
  15. package/dist/compiler/expression.d.ts +55 -0
  16. package/dist/compiler/expression.js +858 -0
  17. package/dist/compiler/index.d.ts +8 -0
  18. package/dist/compiler/index.js +50 -0
  19. package/dist/compiler/ir.d.ts +4 -0
  20. package/dist/compiler/ir.js +255 -0
  21. package/dist/compiler/parser.d.ts +3 -0
  22. package/dist/compiler/parser.js +236 -0
  23. package/dist/compiler/sfc.d.ts +33 -0
  24. package/dist/compiler/sfc.js +439 -0
  25. package/dist/compiler/targets/client.d.ts +3 -0
  26. package/dist/compiler/targets/client.js +568 -0
  27. package/dist/compiler/targets/server.d.ts +7 -0
  28. package/dist/compiler/targets/server.js +396 -0
  29. package/dist/compiler/targets/stream.d.ts +2 -0
  30. package/dist/compiler/targets/stream.js +204 -0
  31. package/dist/compiler/types.d.ts +185 -0
  32. package/dist/compiler/types.js +1 -0
  33. package/dist/compiler/utils.d.ts +40 -0
  34. package/dist/compiler/utils.js +185 -0
  35. package/dist/compiler/whitespace.d.ts +2 -0
  36. package/dist/compiler/whitespace.js +66 -0
  37. package/dist/compiler.d.ts +1 -0
  38. package/dist/compiler.js +1 -0
  39. package/dist/constant-time.d.ts +1 -0
  40. package/dist/constant-time.js +26 -0
  41. package/dist/cookies.d.ts +44 -0
  42. package/dist/cookies.js +199 -0
  43. package/dist/diagnostics.d.ts +22 -0
  44. package/dist/diagnostics.js +45 -0
  45. package/dist/env.d.ts +27 -0
  46. package/dist/env.js +37 -0
  47. package/dist/html-escape.d.ts +2 -0
  48. package/dist/html-escape.js +20 -0
  49. package/dist/html-whitespace.d.ts +6 -0
  50. package/dist/html-whitespace.js +83 -0
  51. package/dist/i18n.d.ts +19 -0
  52. package/dist/i18n.js +93 -0
  53. package/dist/index.d.ts +20 -0
  54. package/dist/index.js +18 -0
  55. package/dist/language-server.d.ts +17 -0
  56. package/dist/language-server.js +79 -0
  57. package/dist/package-integrity.d.ts +9 -0
  58. package/dist/package-integrity.js +64 -0
  59. package/dist/result.d.ts +13 -0
  60. package/dist/result.js +4 -0
  61. package/dist/router-node.d.ts +2 -0
  62. package/dist/router-node.js +12 -0
  63. package/dist/router.d.ts +324 -0
  64. package/dist/router.js +1024 -0
  65. package/dist/runtime/attr.d.ts +3 -0
  66. package/dist/runtime/attr.js +66 -0
  67. package/dist/runtime/class.d.ts +2 -0
  68. package/dist/runtime/class.js +10 -0
  69. package/dist/runtime/conditional.d.ts +78 -0
  70. package/dist/runtime/conditional.js +140 -0
  71. package/dist/runtime/enhancement.d.ts +11 -0
  72. package/dist/runtime/enhancement.js +70 -0
  73. package/dist/runtime/error-boundary.d.ts +8 -0
  74. package/dist/runtime/error-boundary.js +27 -0
  75. package/dist/runtime/event.d.ts +1 -0
  76. package/dist/runtime/event.js +11 -0
  77. package/dist/runtime/form.d.ts +41 -0
  78. package/dist/runtime/form.js +167 -0
  79. package/dist/runtime/fragment.d.ts +6 -0
  80. package/dist/runtime/fragment.js +18 -0
  81. package/dist/runtime/html.d.ts +8 -0
  82. package/dist/runtime/html.js +6 -0
  83. package/dist/runtime/hydrate.d.ts +68 -0
  84. package/dist/runtime/hydrate.js +205 -0
  85. package/dist/runtime/keyed-rows.d.ts +61 -0
  86. package/dist/runtime/keyed-rows.js +192 -0
  87. package/dist/runtime/list.d.ts +84 -0
  88. package/dist/runtime/list.js +424 -0
  89. package/dist/runtime/portal.d.ts +6 -0
  90. package/dist/runtime/portal.js +12 -0
  91. package/dist/runtime/router.d.ts +102 -0
  92. package/dist/runtime/router.js +649 -0
  93. package/dist/runtime/signal.d.ts +30 -0
  94. package/dist/runtime/signal.js +275 -0
  95. package/dist/runtime/store.d.ts +1 -0
  96. package/dist/runtime/store.js +1 -0
  97. package/dist/runtime/stream-client.d.ts +8 -0
  98. package/dist/runtime/stream-client.js +33 -0
  99. package/dist/runtime/text.d.ts +2 -0
  100. package/dist/runtime/text.js +10 -0
  101. package/dist/runtime/virtual-list.d.ts +15 -0
  102. package/dist/runtime/virtual-list.js +89 -0
  103. package/dist/security.d.ts +27 -0
  104. package/dist/security.js +173 -0
  105. package/dist/server/form-action.d.ts +52 -0
  106. package/dist/server/form-action.js +102 -0
  107. package/dist/server/html.d.ts +19 -0
  108. package/dist/server/html.js +101 -0
  109. package/dist/server/stream.d.ts +4 -0
  110. package/dist/server/stream.js +72 -0
  111. package/dist/source-map.d.ts +17 -0
  112. package/dist/source-map.js +18 -0
  113. package/dist/tachyon-html.d.ts +110 -0
  114. package/dist/testing.d.ts +14 -0
  115. package/dist/testing.js +36 -0
  116. package/dist/typed.d.ts +9 -0
  117. package/dist/typed.js +4 -0
  118. package/dist/vite.d.ts +84 -0
  119. package/dist/vite.js +426 -0
  120. package/package.json +604 -0
@@ -0,0 +1,272 @@
1
+ import { renderRouteWithBindings, renderRouteStreamWithBindings, } from "../router.js";
2
+ const mergeHeaders = (base, extra) => {
3
+ const headers = new Headers(base);
4
+ extra?.forEach((value, key) => headers.set(key, value));
5
+ return headers;
6
+ };
7
+ const withExtraHeaders = (response, ...extras) => {
8
+ let headers = new Headers(response.headers);
9
+ for (const extra of extras) {
10
+ headers = mergeHeaders(headers, extra);
11
+ }
12
+ return new Response(response.body, {
13
+ status: response.status,
14
+ statusText: response.statusText,
15
+ headers,
16
+ });
17
+ };
18
+ export const defineStaticRoute = (route) => route;
19
+ const findStaticRoute = (routes, method, url) => routes?.find((route) => {
20
+ const methods = route.methods ?? ["GET", "HEAD"];
21
+ const path = route.path.includes("?") ? `${url.pathname}${url.search}` : url.search ? "" : url.pathname;
22
+ return methods.includes(method) && route.path === path;
23
+ });
24
+ const byteLength = (value) => new TextEncoder().encode(value).byteLength;
25
+ const headersForStaticRoute = (route, extra) => {
26
+ const headers = mergeHeaders(new Headers(route.headers), extra);
27
+ if (!headers.has("content-type")) {
28
+ headers.set("content-type", "text/html; charset=utf-8");
29
+ }
30
+ if (!headers.has("content-length")) {
31
+ headers.set("content-length", String(byteLength(route.body)));
32
+ }
33
+ return headers;
34
+ };
35
+ const responseForStaticRoute = (route, request, securityHeaders) => new Response(request.method === "HEAD" ? null : route.body, {
36
+ status: route.status ?? 200,
37
+ headers: headersForStaticRoute(route, securityHeaders),
38
+ });
39
+ const now = () => globalThis.performance?.now() ?? Date.now();
40
+ const routePatternFor = (match) => match.branch.at(-1)?.path ?? match.pathname;
41
+ const routeIdFor = (match) => match.route.id ?? routePatternFor(match);
42
+ const createRequestState = (request, observability) => {
43
+ const url = new URL(request.url);
44
+ return {
45
+ request,
46
+ method: request.method,
47
+ path: url.pathname,
48
+ adapter: observability?.adapter ?? "workers",
49
+ ...(observability?.runtime ? { runtime: observability.runtime } : {}),
50
+ ...(observability?.metadata ? { metadata: observability.metadata } : {}),
51
+ startedAt: now(),
52
+ };
53
+ };
54
+ const emitRequestStart = async (observability, state) => {
55
+ await observability?.onRequestStart?.({ type: "request_start", ...state });
56
+ };
57
+ const emitRouteMatched = async (observability, state, match) => {
58
+ const routePattern = routePatternFor(match);
59
+ state.routeId = routeIdFor(match);
60
+ state.routePattern = routePattern;
61
+ await observability?.onRouteMatched?.({
62
+ type: "route_matched",
63
+ ...state,
64
+ routeId: state.routeId,
65
+ routePattern,
66
+ matchedRoute: match,
67
+ });
68
+ };
69
+ const emitResponse = async (observability, state, response, failedBeforeRouteDispatch) => {
70
+ await observability?.onResponse?.({
71
+ type: "response",
72
+ ...state,
73
+ status: response.status,
74
+ failedBeforeRouteDispatch,
75
+ durationMs: now() - state.startedAt,
76
+ });
77
+ };
78
+ const emitError = async (observability, state, error, failedBeforeRouteDispatch) => {
79
+ await observability?.onError?.({
80
+ type: "error",
81
+ ...state,
82
+ error,
83
+ failedBeforeRouteDispatch,
84
+ durationMs: now() - state.startedAt,
85
+ });
86
+ };
87
+ const routeObservabilityHooks = (hooks, observability, state) => {
88
+ if (!observability) {
89
+ return hooks;
90
+ }
91
+ return {
92
+ ...hooks,
93
+ onMatch: async (context) => {
94
+ await hooks?.onMatch?.(context);
95
+ await emitRouteMatched(observability, state, context.match);
96
+ },
97
+ onError: async (context) => {
98
+ await hooks?.onError?.(context);
99
+ await emitError(observability, state, context.error, false);
100
+ },
101
+ };
102
+ };
103
+ const normalizeBasePath = (basePath) => {
104
+ if (!basePath || basePath === "/") {
105
+ return basePath;
106
+ }
107
+ return basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
108
+ };
109
+ const matchesBasePath = (pathname, basePath) => {
110
+ const normalized = normalizeBasePath(basePath);
111
+ return !normalized || normalized === "/" || pathname === normalized || pathname.startsWith(`${normalized}/`);
112
+ };
113
+ const isWorkersAssetsBinding = (value) => Boolean(value && typeof value === "object" && typeof value.fetch === "function");
114
+ const resolveAssetsBinding = (options, env) => {
115
+ if (!options) {
116
+ return undefined;
117
+ }
118
+ if (options.binding) {
119
+ return options.binding;
120
+ }
121
+ const bindingName = options.bindingName ?? "ASSETS";
122
+ const candidate = env && typeof env === "object" ? env[bindingName] : undefined;
123
+ return isWorkersAssetsBinding(candidate) ? candidate : undefined;
124
+ };
125
+ const responseForAsset = async (options, request, env) => {
126
+ const assetOptions = options.assets;
127
+ if (!assetOptions) {
128
+ return undefined;
129
+ }
130
+ const url = new URL(request.url);
131
+ if (!matchesBasePath(url.pathname, assetOptions.basePath)) {
132
+ return undefined;
133
+ }
134
+ if (request.method !== "GET" && request.method !== "HEAD") {
135
+ return withExtraHeaders(new Response("Method Not Allowed", { status: 405, headers: { allow: "GET, HEAD" } }), options.securityHeaders);
136
+ }
137
+ const binding = resolveAssetsBinding(assetOptions, env);
138
+ if (!binding) {
139
+ return undefined;
140
+ }
141
+ const response = await binding.fetch(request);
142
+ const fallthroughStatuses = assetOptions.fallthroughStatuses ?? (assetOptions.basePath ? [] : [404]);
143
+ if (fallthroughStatuses.includes(response.status)) {
144
+ return undefined;
145
+ }
146
+ return withExtraHeaders(response, new Headers(assetOptions.headers), options.securityHeaders);
147
+ };
148
+ export const workersStreamFromChunks = (chunks) => {
149
+ const iterator = chunks[Symbol.asyncIterator]();
150
+ const encoder = new TextEncoder();
151
+ let pendingHighSurrogate = "";
152
+ return new ReadableStream({
153
+ async pull(controller) {
154
+ while (true) {
155
+ const next = await iterator.next();
156
+ if (next.done) {
157
+ if (pendingHighSurrogate)
158
+ controller.enqueue(encoder.encode(pendingHighSurrogate));
159
+ pendingHighSurrogate = "";
160
+ controller.close();
161
+ return;
162
+ }
163
+ let value = `${pendingHighSurrogate}${next.value}`;
164
+ pendingHighSurrogate = "";
165
+ const finalCodeUnit = value.charCodeAt(value.length - 1);
166
+ if (finalCodeUnit >= 0xd800 && finalCodeUnit <= 0xdbff) {
167
+ pendingHighSurrogate = value.slice(-1);
168
+ value = value.slice(0, -1);
169
+ }
170
+ if (value.length > 0) {
171
+ controller.enqueue(encoder.encode(value));
172
+ return;
173
+ }
174
+ }
175
+ },
176
+ async cancel() {
177
+ await iterator.return?.();
178
+ },
179
+ });
180
+ };
181
+ const responseFor = async (options, request, env) => {
182
+ const state = createRequestState(request, options.observability);
183
+ await emitRequestStart(options.observability, state);
184
+ const staticRoute = findStaticRoute(options.staticRoutes, request.method, new URL(request.url));
185
+ if (staticRoute) {
186
+ state.routeId = staticRoute.path;
187
+ state.routePattern = staticRoute.path;
188
+ const response = responseForStaticRoute(staticRoute, request, options.securityHeaders);
189
+ await emitResponse(options.observability, state, response, false);
190
+ return response;
191
+ }
192
+ try {
193
+ const asset = await responseForAsset(options, request, env);
194
+ if (asset) {
195
+ const response = asset;
196
+ await emitResponse(options.observability, state, response, false);
197
+ return response;
198
+ }
199
+ const hooks = routeObservabilityHooks(options.hooks, options.observability, state);
200
+ const renderOptions = {
201
+ ...options,
202
+ bindings: env,
203
+ middleware: options.middleware,
204
+ ...(hooks ? { hooks } : {}),
205
+ };
206
+ if (options.streaming) {
207
+ const result = await renderRouteStreamWithBindings(options.routes, request, renderOptions);
208
+ if (!result.ok) {
209
+ const response = new Response(result.error.message, { status: result.error.status });
210
+ await emitResponse(options.observability, state, response, true);
211
+ return response;
212
+ }
213
+ const stream = workersStreamFromChunks(result.value.chunks);
214
+ const response = new Response(stream, {
215
+ status: result.value.status,
216
+ headers: mergeHeaders(result.value.headers, options.securityHeaders),
217
+ });
218
+ await emitResponse(options.observability, state, response, state.routeId === undefined);
219
+ return response;
220
+ }
221
+ const result = await renderRouteWithBindings(options.routes, request, renderOptions);
222
+ if (!result.ok) {
223
+ const response = new Response(result.error.message, { status: result.error.status });
224
+ await emitResponse(options.observability, state, response, true);
225
+ return response;
226
+ }
227
+ const response = new Response(result.value.responseBody ?? result.value.html, {
228
+ status: result.value.status,
229
+ headers: mergeHeaders(result.value.headers, options.securityHeaders),
230
+ });
231
+ await emitResponse(options.observability, state, response, state.routeId === undefined);
232
+ return response;
233
+ }
234
+ catch (error) {
235
+ await emitError(options.observability, state, error, state.routeId === undefined);
236
+ throw error;
237
+ }
238
+ };
239
+ const responseForFetch = async (options, request, env) => {
240
+ const state = createRequestState(request, options.observability);
241
+ await emitRequestStart(options.observability, state);
242
+ const staticRoute = findStaticRoute(options.staticRoutes, request.method, new URL(request.url));
243
+ if (staticRoute) {
244
+ state.routeId = staticRoute.path;
245
+ state.routePattern = staticRoute.path;
246
+ const response = responseForStaticRoute(staticRoute, request, options.securityHeaders);
247
+ await emitResponse(options.observability, state, response, false);
248
+ return response;
249
+ }
250
+ try {
251
+ const asset = await responseForAsset(options, request, env);
252
+ if (asset) {
253
+ await emitResponse(options.observability, state, asset, false);
254
+ return asset;
255
+ }
256
+ const response = withExtraHeaders(await options.fetch(request), options.securityHeaders);
257
+ await emitResponse(options.observability, state, response, false);
258
+ return response;
259
+ }
260
+ catch (error) {
261
+ await emitError(options.observability, state, error, state.routeId === undefined);
262
+ throw error;
263
+ }
264
+ };
265
+ export function createWorkersHandler(options) {
266
+ return {
267
+ fetch: (request, env) => responseFor(options, request, env),
268
+ };
269
+ }
270
+ export const createWorkersFetchHandler = (options) => ({
271
+ fetch: (request, env) => responseForFetch(options, request, env),
272
+ });
@@ -0,0 +1,3 @@
1
+ export { createLambdaFetchHandler, createLambdaHandler, createLambdaStreamingHandler, lambdaResponseFromWebResponse, requestFromLambdaEvent, writeWebResponseToLambdaStream, type LambdaFetchHandlerOptions, type LambdaHandlerOptions, type LambdaContextMetadata, type LambdaHttpEventV2, type LambdaHttpResponseMetadata, type LambdaProxyResponseV2, type LambdaResponseStream, type LambdaStreamingRuntime, } from "./adapters/lambda.js";
2
+ export { createWorkersFetchHandler, createWorkersHandler, defineStaticRoute, type AdapterFetchHandler, type AdapterErrorEvent, type AdapterObservabilityHooks, type AdapterObservabilityMetadata, type AdapterRequestStartEvent, type AdapterResponseEvent, type AdapterRouteMatchedEvent, type StaticRouteDefinition, type WorkersFetchHandlerOptions, type WorkersAssetOptions, type WorkersAssetsBinding, type WorkersHandlerOptions, } from "./adapters/workers.js";
3
+ export { createNodeFetchHandler, createNodeHandler, createStaticAssetHandler, writeNodeResponse, type NodeFetchHandlerOptions, type NodeHandlerOptions, type StaticAssetOptions, } from "./adapters/node.js";
@@ -0,0 +1,3 @@
1
+ export { createLambdaFetchHandler, createLambdaHandler, createLambdaStreamingHandler, lambdaResponseFromWebResponse, requestFromLambdaEvent, writeWebResponseToLambdaStream, } from "./adapters/lambda.js";
2
+ export { createWorkersFetchHandler, createWorkersHandler, defineStaticRoute, } from "./adapters/workers.js";
3
+ export { createNodeFetchHandler, createNodeHandler, createStaticAssetHandler, writeNodeResponse, } from "./adapters/node.js";
package/dist/app.d.ts ADDED
@@ -0,0 +1,98 @@
1
+ import type { TemplateWhitespacePolicy } from "./compiler/types.js";
2
+ import { type Result } from "./result.js";
3
+ import type { TemplateScope, TypedTemplate } from "./typed.js";
4
+ import { normalizeHtmlTagWhitespace, type HtmlWhitespacePolicy } from "./html-whitespace.js";
5
+ export type { HtmlWhitespacePolicy } from "./html-whitespace.js";
6
+ export { condenseHtmlWhitespace, normalizeHtmlTagWhitespace } from "./html-whitespace.js";
7
+ type TachyonAppPageBase = {
8
+ path: string;
9
+ fileName: string;
10
+ title?: string;
11
+ assetPrefix?: string;
12
+ templateWhitespace?: TemplateWhitespacePolicy;
13
+ };
14
+ export type TachyonAppPage<Scope extends TemplateScope = TemplateScope> = TachyonAppPageBase & ({
15
+ template: string;
16
+ scope?: Record<string, unknown>;
17
+ } | {
18
+ template: TypedTemplate<Scope>;
19
+ scope: Scope;
20
+ });
21
+ export type TachyonAppPageFile = {
22
+ path: string;
23
+ fileName: string;
24
+ file: string;
25
+ assetPrefix?: string;
26
+ templateWhitespace?: TemplateWhitespacePolicy;
27
+ };
28
+ export type TachyonAppDefinition<Pages extends readonly TachyonAppPage<any>[] = readonly TachyonAppPage[]> = {
29
+ lang?: string;
30
+ title?: string | ((page: TachyonAppPage) => string);
31
+ pages: Pages;
32
+ shell?: (context: TachyonAppShellContext) => string;
33
+ assets?: TachyonAppAssets | ((page: TachyonAppPage) => TachyonAppAssets);
34
+ notFound?: TachyonAppNotFoundPage;
35
+ templateWhitespace?: TemplateWhitespacePolicy;
36
+ };
37
+ export type TachyonAppNotFoundPage = {
38
+ title?: string;
39
+ assets?: TachyonAppAssets;
40
+ render: (context: {
41
+ path: string;
42
+ }) => string;
43
+ };
44
+ export type TachyonAppAssets = {
45
+ scripts?: readonly string[];
46
+ styles?: readonly string[];
47
+ };
48
+ export type TachyonAppShellContext = {
49
+ page: TachyonAppPage;
50
+ routeHtml: string;
51
+ };
52
+ export type TachyonAppDocumentOptions = {
53
+ assets?: TachyonAppAssets;
54
+ whitespace?: HtmlWhitespacePolicy;
55
+ /** @deprecated Use `whitespace: "normalize-tags"` instead. */
56
+ minify?: boolean;
57
+ };
58
+ export type TachyonAppHtmlEntry = {
59
+ fileName: string;
60
+ source: string;
61
+ path: string;
62
+ };
63
+ export type TachyonAppRenderResult = {
64
+ status: 200 | 404;
65
+ html: string;
66
+ };
67
+ export type TachyonApp = {
68
+ pages: readonly TachyonAppPage[];
69
+ pageForPath: (path: string) => TachyonAppPage | undefined;
70
+ renderRoute: (path: string) => string;
71
+ renderShell: (path: string) => string;
72
+ renderDocument: (path: string, options?: TachyonAppDocumentOptions) => string;
73
+ renderResponse: (path: string, options?: TachyonAppDocumentOptions) => TachyonAppRenderResult;
74
+ entries: (options?: TachyonAppDocumentOptions) => TachyonAppHtmlEntry[];
75
+ };
76
+ type ValidateTypedPageScopes<Pages extends readonly TachyonAppPage<any>[]> = {
77
+ readonly [Index in keyof Pages]: Pages[Index] extends {
78
+ template: TypedTemplate<infer Scope>;
79
+ scope: infer Given;
80
+ } ? Given extends Scope ? Pages[Index] : never : Pages[Index];
81
+ };
82
+ export type TemplateTypeOptions = {
83
+ typeName?: string;
84
+ };
85
+ export declare const normalizeAppPath: (path: string) => string;
86
+ /** @deprecated Use `normalizeHtmlTagWhitespace()`. */
87
+ export declare const minifyHtml: typeof normalizeHtmlTagWhitespace;
88
+ export declare const renderAppDocument: (app: TachyonApp, path: string, options?: TachyonAppDocumentOptions) => string;
89
+ export declare const renderAppResponse: (app: TachyonApp, path: string, options?: TachyonAppDocumentOptions) => TachyonAppRenderResult;
90
+ export declare const defineApp: <const Pages extends readonly TachyonAppPage<any>[]>(definition: Omit<TachyonAppDefinition<Pages>, "pages"> & {
91
+ pages: ValidateTypedPageScopes<Pages>;
92
+ }) => TachyonApp;
93
+ export declare const pagesFromRouteFiles: (files: readonly string[], options: {
94
+ rootDir: string;
95
+ templateWhitespace?: TemplateWhitespacePolicy;
96
+ }) => TachyonAppPageFile[];
97
+ export declare const generateTemplateTypes: (source: string, options?: TemplateTypeOptions) => Result<string, string>;
98
+ export declare const generateTachyonModuleTypes: (source: string, options?: TemplateTypeOptions) => Result<string, string>;
package/dist/app.js ADDED
@@ -0,0 +1,306 @@
1
+ import { compileServerTemplate } from "./compiler/index.js";
2
+ import { compileTachyonSfc, extractStaticSfcScope, generateSfcScriptDeclarations } from "./compiler/sfc.js";
3
+ import { escapeHtml } from "./html-escape.js";
4
+ import { err, ok } from "./result.js";
5
+ import { applyHtmlWhitespace, normalizeHtmlTagWhitespace } from "./html-whitespace.js";
6
+ export { condenseHtmlWhitespace, normalizeHtmlTagWhitespace } from "./html-whitespace.js";
7
+ const templateExtensions = /\.(?:td|tachyon(?:\.html)?)$/;
8
+ const ignoredIdentifiers = new Set([
9
+ "Array",
10
+ "Boolean",
11
+ "Date",
12
+ "Math",
13
+ "Number",
14
+ "Object",
15
+ "String",
16
+ "false",
17
+ "null",
18
+ "true",
19
+ "undefined",
20
+ ]);
21
+ export const normalizeAppPath = (path) => {
22
+ if (path === "" || path === "/" || path === "/index.html") {
23
+ return "/";
24
+ }
25
+ const withoutIndex = path.endsWith("/index.html") ? path.slice(0, -"index.html".length) : path;
26
+ return withoutIndex.endsWith("/") ? withoutIndex : `${withoutIndex}/`;
27
+ };
28
+ const appPathMatches = (pattern, path) => {
29
+ const patternSegments = normalizeAppPath(pattern).split("/").filter(Boolean);
30
+ const pathSegments = normalizeAppPath(path).split("/").filter(Boolean);
31
+ for (let index = 0; index < patternSegments.length; index += 1) {
32
+ const patternSegment = patternSegments[index];
33
+ if (patternSegment?.startsWith("*"))
34
+ return pathSegments.length >= index + 1;
35
+ const pathSegment = pathSegments[index];
36
+ if (pathSegment === undefined)
37
+ return false;
38
+ if (!patternSegment?.startsWith(":") && patternSegment !== pathSegment)
39
+ return false;
40
+ }
41
+ return patternSegments.length === pathSegments.length;
42
+ };
43
+ /** @deprecated Use `normalizeHtmlTagWhitespace()`. */
44
+ export const minifyHtml = normalizeHtmlTagWhitespace;
45
+ const templateSource = (template) => typeof template === "string" ? template : template.source;
46
+ const compilePage = (page, templateWhitespace) => {
47
+ const result = compileTachyonSfc(templateSource(page.template), { whitespace: templateWhitespace });
48
+ if (!result.ok) {
49
+ throw new Error(result.error.message);
50
+ }
51
+ return result.value.template;
52
+ };
53
+ const compilePageRenderer = (page, templateWhitespace) => compileServerTemplate(compilePage(page, templateWhitespace));
54
+ const scopeForPage = (page) => {
55
+ if (page.scope)
56
+ return page.scope;
57
+ const result = extractStaticSfcScope(templateSource(page.template));
58
+ if (!result.ok)
59
+ throw new Error(result.error);
60
+ return result.value ?? {};
61
+ };
62
+ const defaultShell = ({ routeHtml }) => `<main id="app">${routeHtml}</main>`;
63
+ const assetsForPage = (app, page, override) => {
64
+ if (override) {
65
+ return override;
66
+ }
67
+ return typeof app.assets === "function" ? app.assets(page) : (app.assets ?? {});
68
+ };
69
+ const titleForPage = (app, page) => {
70
+ if (typeof app.title === "function") {
71
+ return app.title(page);
72
+ }
73
+ return page.title ?? app.title ?? "Tachyon App";
74
+ };
75
+ export const renderAppDocument = (app, path, options = {}) => app.renderDocument(path, options);
76
+ export const renderAppResponse = (app, path, options = {}) => app.renderResponse(path, options);
77
+ export const defineApp = (definition) => {
78
+ const pages = definition.pages.map((page) => ({
79
+ ...page,
80
+ path: normalizeAppPath(page.path),
81
+ }));
82
+ const conflicts = new Map();
83
+ for (const page of pages) {
84
+ for (const key of [`path ${page.path}`, `file ${page.fileName}`]) {
85
+ const group = conflicts.get(key) ?? [];
86
+ group.push(page);
87
+ conflicts.set(key, group);
88
+ }
89
+ }
90
+ const duplicates = [...conflicts.entries()].filter(([, group]) => group.length > 1);
91
+ if (duplicates.length > 0) {
92
+ throw new Error(`Duplicate app pages:\n${duplicates.map(([key, group]) => `${key}: ${group.map((page) => page.fileName).join(", ")}`).join("\n")}`);
93
+ }
94
+ const pagesByPath = new Map();
95
+ for (const page of pages) {
96
+ if (!pagesByPath.has(page.path)) {
97
+ pagesByPath.set(page.path, page);
98
+ }
99
+ }
100
+ const renderers = new Map();
101
+ const pageForPath = (path) => {
102
+ const normalized = normalizeAppPath(path);
103
+ return pagesByPath.get(normalized) ?? pages.find((page) => appPathMatches(page.path, normalized));
104
+ };
105
+ const renderRoute = (path) => {
106
+ const page = pageForPath(path);
107
+ if (!page) {
108
+ return "";
109
+ }
110
+ const render = renderers.get(page.path) ??
111
+ compilePageRenderer(page, page.templateWhitespace ?? definition.templateWhitespace ?? "preserve");
112
+ renderers.set(page.path, render);
113
+ return render(scopeForPage(page));
114
+ };
115
+ const renderShell = (path) => {
116
+ const page = pageForPath(path);
117
+ if (!page) {
118
+ return defaultShell({ page: { path: "/", fileName: "index.html", template: "" }, routeHtml: "" });
119
+ }
120
+ const routeHtml = renderRoute(page.path);
121
+ return (definition.shell ?? defaultShell)({ page, routeHtml });
122
+ };
123
+ const documentFor = (page, routeHtml, options, assetsOverride) => {
124
+ const assets = assetsForPage(definition, page, options.assets ?? assetsOverride);
125
+ const shell = (definition.shell ?? defaultShell)({ page, routeHtml });
126
+ const html = `<!doctype html>
127
+ <html lang="${escapeHtml(definition.lang ?? "en")}">
128
+ <head>
129
+ <meta charset="UTF-8" />
130
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
131
+ <title>${escapeHtml(titleForPage(definition, page))}</title>
132
+ ${(assets.styles ?? []).map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}" />`).join("\n ")}
133
+ ${(assets.scripts ?? []).map((src) => `<script type="module" src="${escapeHtml(src)}"></script>`).join("\n ")}
134
+ </head>
135
+ <body>
136
+ ${shell}
137
+ </body>
138
+ </html>
139
+ `;
140
+ const whitespace = options.whitespace ?? (options.minify ? "normalize-tags" : "preserve-tags");
141
+ return applyHtmlWhitespace(html, whitespace);
142
+ };
143
+ const renderDocument = (path, options = {}) => {
144
+ const page = pageForPath(path);
145
+ if (!page) {
146
+ throw new Error(`No page found for ${path}.`);
147
+ }
148
+ return documentFor(page, renderRoute(page.path), options);
149
+ };
150
+ const renderResponse = (path, options = {}) => {
151
+ const page = pageForPath(path);
152
+ if (page) {
153
+ return { status: 200, html: renderDocument(page.path, options) };
154
+ }
155
+ const fallback = definition.notFound;
156
+ const fallbackPage = {
157
+ path: normalizeAppPath(path),
158
+ fileName: "404.html",
159
+ title: fallback?.title ?? "Not Found",
160
+ template: "",
161
+ };
162
+ const routeHtml = fallback?.render({ path }) ?? "<h1>Not Found</h1>";
163
+ return { status: 404, html: documentFor(fallbackPage, routeHtml, options, fallback?.assets) };
164
+ };
165
+ return {
166
+ pages,
167
+ pageForPath,
168
+ renderDocument,
169
+ renderResponse,
170
+ renderRoute,
171
+ renderShell,
172
+ entries: (options = {}) => pages.map((page) => ({
173
+ fileName: page.fileName,
174
+ path: page.path,
175
+ source: renderDocument(page.path, options),
176
+ })),
177
+ };
178
+ };
179
+ const routeSegmentsForFile = (file, rootDir) => {
180
+ const relative = file.startsWith(rootDir) ? file.slice(rootDir.length).replace(/^[/\\]/, "") : file;
181
+ const withoutExtension = relative.replace(templateExtensions, "");
182
+ const parts = withoutExtension.split(/[/\\]/).filter(Boolean);
183
+ if (parts.at(-1) === "page") {
184
+ parts.pop();
185
+ }
186
+ return parts.filter((part) => part !== "index");
187
+ };
188
+ const segmentToRoute = (segment) => {
189
+ if (/^\[\.\.\.[^[\]]+\]$/.test(segment)) {
190
+ return `*${segment.slice(4, -1)}`;
191
+ }
192
+ if (/^\[[^[\]]+\]$/.test(segment)) {
193
+ return `:${segment.slice(1, -1)}`;
194
+ }
195
+ return segment;
196
+ };
197
+ const segmentToFileName = (segment) => segment;
198
+ const assetPrefixForDepth = (depth) => depth <= 0 ? "." : Array.from({ length: depth }, () => "..").join("/");
199
+ export const pagesFromRouteFiles = (files, options) => files
200
+ .filter((file) => templateExtensions.test(file))
201
+ .map((file) => {
202
+ const segments = routeSegmentsForFile(file, options.rootDir);
203
+ const path = segments.length === 0 ? "/" : `/${segments.map(segmentToRoute).join("/")}/`;
204
+ const fileName = segments.length === 0 ? "index.html" : `${segments.map(segmentToFileName).join("/")}/index.html`;
205
+ return {
206
+ assetPrefix: assetPrefixForDepth(segments.length),
207
+ file,
208
+ fileName,
209
+ path,
210
+ ...(options.templateWhitespace ? { templateWhitespace: options.templateWhitespace } : {}),
211
+ };
212
+ });
213
+ const collectExpressionIdentifiers = (expression, identifiers) => {
214
+ for (const match of expression.matchAll(/\b[$A-Z_a-z][$\w]*\b/g)) {
215
+ const name = match[0];
216
+ const previous = expression[match.index - 1];
217
+ if (previous === "." || ignoredIdentifiers.has(name)) {
218
+ continue;
219
+ }
220
+ identifiers.add(name);
221
+ }
222
+ };
223
+ const collectBindingIdentifiers = (binding, identifiers) => {
224
+ if (binding.kind === "event") {
225
+ collectExpressionIdentifiers(binding.handler, identifiers);
226
+ return;
227
+ }
228
+ if (binding.kind === "list") {
229
+ collectExpressionIdentifiers(binding.each, identifiers);
230
+ collectExpressionIdentifiers(binding.key, identifiers);
231
+ for (const child of binding.bindings) {
232
+ collectBindingIdentifiers(child, identifiers);
233
+ }
234
+ identifiers.delete(binding.itemName);
235
+ return;
236
+ }
237
+ if (binding.kind === "if") {
238
+ collectExpressionIdentifiers(binding.test, identifiers);
239
+ for (const child of binding.bindings) {
240
+ collectBindingIdentifiers(child, identifiers);
241
+ }
242
+ return;
243
+ }
244
+ collectExpressionIdentifiers(binding.expression, identifiers);
245
+ };
246
+ const collectBindingRequirements = (binding, fields) => {
247
+ const identifiers = new Set();
248
+ collectBindingIdentifiers(binding, identifiers);
249
+ for (const identifier of identifiers) {
250
+ if (!fields.has(identifier))
251
+ fields.set(identifier, "unknown");
252
+ }
253
+ if (binding.kind === "event" && /^[$A-Z_a-z][$\w]*$/.test(binding.handler)) {
254
+ fields.set(binding.handler, "(event: Event) => unknown");
255
+ }
256
+ };
257
+ export const generateTemplateTypes = (source, options = {}) => {
258
+ const result = compileTachyonSfc(source);
259
+ if (!result.ok) {
260
+ return err(result.error.message);
261
+ }
262
+ const identifiers = new Set();
263
+ for (const binding of result.value.template.client.bindings) {
264
+ collectBindingIdentifiers(binding, identifiers);
265
+ }
266
+ for (const store of result.value.template.client.stores) {
267
+ identifiers.add(store.name);
268
+ }
269
+ const typeName = options.typeName ?? "TemplateScope";
270
+ const fields = Array.from(identifiers)
271
+ .sort()
272
+ .map((name) => ` ${name}: unknown;`)
273
+ .join("\n");
274
+ return ok(`export type ${typeName} = {\n${fields ? `${fields}\n` : ""}};\n`);
275
+ };
276
+ export const generateTachyonModuleTypes = (source, options = {}) => {
277
+ const result = compileTachyonSfc(source);
278
+ if (!result.ok) {
279
+ return err(result.error.message);
280
+ }
281
+ const scriptTypes = generateSfcScriptDeclarations(result.value.descriptor.script);
282
+ if (!scriptTypes.ok) {
283
+ return err(scriptTypes.error);
284
+ }
285
+ const typeName = options.typeName ?? "TemplateScope";
286
+ const fields = new Map();
287
+ for (const binding of result.value.template.client.bindings)
288
+ collectBindingRequirements(binding, fields);
289
+ for (const store of result.value.template.client.stores)
290
+ fields.set(store.name, "unknown");
291
+ const requiredType = `type __TachyonRequiredScope = {\n${Array.from(fields)
292
+ .sort(([left], [right]) => left.localeCompare(right))
293
+ .map(([name, type]) => ` ${name}: ${type};`)
294
+ .join("\n")}\n};`;
295
+ const hasNamedScope = /\bexport\s+declare\s+const\s+scope\s*:/.test(scriptTypes.value);
296
+ const scopeType = hasNamedScope
297
+ ? `type __TachyonAssertScope<T extends __TachyonRequiredScope> = T;\nexport type ${typeName} = __TachyonAssertScope<ReturnType<typeof scope>>;`
298
+ : `export type ${typeName} = __TachyonRequiredScope;`;
299
+ const templateExports = [
300
+ `export declare const templateHtml: string;`,
301
+ `export declare const hydrationBoundaries: unknown[];`,
302
+ `export declare const componentBoundaries: unknown[];`,
303
+ `export declare const bind: (root: Element, scope: ${typeName}) => void | (() => void);`,
304
+ ].join("\n");
305
+ return ok([scriptTypes.value, requiredType, scopeType, templateExports].filter((part) => part.length > 0).join("\n\n") + "\n");
306
+ };