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
package/dist/router.js ADDED
@@ -0,0 +1,1024 @@
1
+ import { escapeHtml } from "./html-escape.js";
2
+ import { err, ok } from "./result.js";
3
+ import { serializeHydrationState } from "./runtime/hydrate.js";
4
+ import { applyHtmlWhitespace, resolveHtmlWhitespacePolicy } from "./html-whitespace.js";
5
+ const routeError = (message, status) => ({ message, status });
6
+ const trimSlashes = (value) => value.replace(/^\/+|\/+$/g, "");
7
+ const joinPaths = (parent, child) => {
8
+ if (child === "*") {
9
+ return "*";
10
+ }
11
+ if (child.startsWith("/")) {
12
+ return child === "/" ? "/" : `/${trimSlashes(child)}`;
13
+ }
14
+ const joined = [trimSlashes(parent), trimSlashes(child)].filter(Boolean).join("/");
15
+ return joined ? `/${joined}` : "/";
16
+ };
17
+ const routeId = (route, path) => route.id ?? path;
18
+ const isRouteResponse = (value) => Boolean(value &&
19
+ typeof value === "object" &&
20
+ value.__tachyonRouteResponse === true);
21
+ const isWebResponse = (value) => value instanceof Response;
22
+ export const isDeferredData = (value) => Boolean(value && typeof value === "object" && value.__tachyonDeferredData === true);
23
+ const routeResponse = (body, init = {}) => ({
24
+ __tachyonRouteResponse: true,
25
+ status: init.status ?? 200,
26
+ headers: new Headers(init.headers),
27
+ body,
28
+ });
29
+ const isSafePathRedirect = (location) => {
30
+ const controlCharacterPattern = /[\u0000-\u001F\u007F]/;
31
+ if (!location.startsWith("/") || location.startsWith("//") || controlCharacterPattern.test(location)) {
32
+ return false;
33
+ }
34
+ try {
35
+ const decoded = decodeURIComponent(location);
36
+ return !decoded.startsWith("//") && !decoded.includes("\\") && !controlCharacterPattern.test(decoded);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ };
42
+ const isApprovedExternalRedirect = (location, allowedOrigins) => {
43
+ if (!allowedOrigins || allowedOrigins.length === 0) {
44
+ return false;
45
+ }
46
+ try {
47
+ const url = new URL(location);
48
+ return (url.protocol === "https:" || url.protocol === "http:") && allowedOrigins.includes(url.origin);
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ };
54
+ export const redirect = (location, init = {}) => {
55
+ if (!isSafePathRedirect(location) &&
56
+ !(init.allowExternal && isApprovedExternalRedirect(location, init.allowedOrigins))) {
57
+ throw new Error(`Unsafe redirect target: ${location}`);
58
+ }
59
+ const headers = new Headers(init.headers);
60
+ headers.set("location", location);
61
+ return routeResponse("", { ...init, status: init.status ?? 302, headers });
62
+ };
63
+ export const json = (data, init = {}) => {
64
+ const headers = new Headers(init.headers);
65
+ if (!headers.has("content-type")) {
66
+ headers.set("content-type", "application/json; charset=utf-8");
67
+ }
68
+ return routeResponse(JSON.stringify(data), { ...init, headers });
69
+ };
70
+ const trustedHtmlValues = new WeakSet();
71
+ export const unsafeHtml = (value) => {
72
+ const trusted = { __tachyonTrustedHtml: true, value };
73
+ trustedHtmlValues.add(trusted);
74
+ return trusted;
75
+ };
76
+ export const escapeToHtml = (value) => unsafeHtml(escapeHtml(value));
77
+ const trustedHtmlValue = (value) => {
78
+ if (typeof value === "string") {
79
+ return escapeHtml(value);
80
+ }
81
+ if (!trustedHtmlValues.has(value)) {
82
+ throw new Error("TrustedHtml values must be created by tachyon-dom helpers.");
83
+ }
84
+ return value.value;
85
+ };
86
+ /**
87
+ * Converts a factory-created TrustedHtml value into a raw progressive response chunk.
88
+ * Route stream chunks are inserted as HTML without adapter escaping. Escape untrusted
89
+ * text with escapeToHtml(), or sanitize intentional markup with a vetted sanitizer,
90
+ * before calling this helper.
91
+ */
92
+ export const trustedHtmlChunk = (value) => trustedHtmlValue(value);
93
+ export const html = (body, init = {}) => {
94
+ const headers = new Headers(init.headers);
95
+ if (!headers.has("content-type")) {
96
+ headers.set("content-type", "text/html; charset=utf-8");
97
+ }
98
+ return routeResponse(trustedHtmlValue(body), { ...init, headers });
99
+ };
100
+ const cacheSeconds = (value) => value !== undefined && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined;
101
+ export const cacheControl = (policy) => {
102
+ const headers = new Headers();
103
+ const directives = [];
104
+ if (policy.mode === "no-store") {
105
+ directives.push("no-store");
106
+ }
107
+ else {
108
+ directives.push(policy.mode ?? "private");
109
+ const maxAge = cacheSeconds(policy.maxAge);
110
+ const sharedMaxAge = cacheSeconds(policy.sharedMaxAge);
111
+ const staleWhileRevalidate = cacheSeconds(policy.staleWhileRevalidate);
112
+ const staleIfError = cacheSeconds(policy.staleIfError);
113
+ if (maxAge !== undefined) {
114
+ directives.push(`max-age=${maxAge}`);
115
+ }
116
+ if (sharedMaxAge !== undefined) {
117
+ directives.push(`s-maxage=${sharedMaxAge}`);
118
+ }
119
+ if (staleWhileRevalidate !== undefined) {
120
+ directives.push(`stale-while-revalidate=${staleWhileRevalidate}`);
121
+ }
122
+ if (staleIfError !== undefined) {
123
+ directives.push(`stale-if-error=${staleIfError}`);
124
+ }
125
+ if (policy.immutable) {
126
+ directives.push("immutable");
127
+ }
128
+ }
129
+ headers.set("cache-control", directives.join(", "));
130
+ if (policy.tags && policy.tags.length > 0) {
131
+ headers.set("cache-tag", policy.tags.join(","));
132
+ }
133
+ return headers;
134
+ };
135
+ export const withCacheHeaders = (response, policy) => {
136
+ const headers = new Headers(response.headers);
137
+ cacheControl(policy).forEach((value, key) => headers.set(key, value));
138
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
139
+ };
140
+ export const defer = (values) => {
141
+ const immediate = {};
142
+ const pending = {};
143
+ for (const [key, value] of Object.entries(values)) {
144
+ if (value instanceof Promise) {
145
+ pending[key] = value;
146
+ }
147
+ else {
148
+ immediate[key] = value;
149
+ }
150
+ }
151
+ return { __tachyonDeferredData: true, immediate, pending };
152
+ };
153
+ export const resolveDeferredData = async (data) => {
154
+ const resolved = { ...data.immediate };
155
+ for (const [key, value] of Object.entries(data.pending)) {
156
+ resolved[key] = await value;
157
+ }
158
+ return resolved;
159
+ };
160
+ const escapeScriptJson = (value) => value.replaceAll("<", "\\u003c").replaceAll(">", "\\u003e");
161
+ const attributeEscapeMap = {
162
+ "&": "&amp;",
163
+ '"': "&quot;",
164
+ "<": "&lt;",
165
+ };
166
+ const escapeAttribute = (value) => value.replace(/[&"<]/g, (char) => attributeEscapeMap[char] ?? char);
167
+ export const renderDeferredDataScript = async (id, data, options = {}) => {
168
+ const resolved = await resolveDeferredData(data);
169
+ const nonce = options.nonce ? ` nonce="${escapeAttribute(options.nonce)}"` : "";
170
+ return `<script type="application/json" data-tachyon-deferred="${escapeAttribute(id)}"${nonce}>${escapeScriptJson(JSON.stringify(resolved) ?? "null")}</script>`;
171
+ };
172
+ const verifyCsrf = async (request, url, env, bindings, options) => options.verify({ request, url, env, bindings });
173
+ const payloadTooLargeResult = (match) => ({
174
+ status: 413,
175
+ html: "<h1>Payload Too Large</h1>",
176
+ headHtml: "",
177
+ resourceHints: "",
178
+ stateScript: "",
179
+ loaderData: {},
180
+ actionResult: undefined,
181
+ headers: new Headers({ "content-type": "text/html; charset=utf-8" }),
182
+ match,
183
+ });
184
+ const readLimitedRequest = async (request, maxBytes) => {
185
+ if (!request.body) {
186
+ return request;
187
+ }
188
+ const reader = request.body.getReader();
189
+ const chunks = [];
190
+ let total = 0;
191
+ while (true) {
192
+ const result = await reader.read();
193
+ if (result.done) {
194
+ break;
195
+ }
196
+ total += result.value.byteLength;
197
+ if (total > maxBytes) {
198
+ await reader.cancel();
199
+ return undefined;
200
+ }
201
+ chunks.push(result.value);
202
+ }
203
+ const body = new Uint8Array(total);
204
+ let offset = 0;
205
+ for (const chunk of chunks) {
206
+ body.set(chunk, offset);
207
+ offset += chunk.byteLength;
208
+ }
209
+ return new Request(request.url, {
210
+ body,
211
+ headers: request.headers,
212
+ method: request.method,
213
+ redirect: request.redirect,
214
+ signal: request.signal,
215
+ });
216
+ };
217
+ const validateCspNonce = (nonce) => {
218
+ if (!/^[A-Za-z0-9+/_=-]+$/.test(nonce)) {
219
+ throw new Error("Invalid CSP nonce.");
220
+ }
221
+ return nonce;
222
+ };
223
+ const hasControlCharacter = (value) => {
224
+ for (const char of value) {
225
+ const code = char.charCodeAt(0);
226
+ if (code <= 0x1f || code === 0x7f) {
227
+ return true;
228
+ }
229
+ }
230
+ return false;
231
+ };
232
+ const validateFrameAncestors = (value) => {
233
+ if (hasControlCharacter(value) || value.includes(";")) {
234
+ throw new Error("Invalid CSP frame-ancestors.");
235
+ }
236
+ const sources = value.split(/\s+/).filter(Boolean);
237
+ if (sources.length === 0 ||
238
+ sources.some((source) => source !== "'self'" && source !== "'none'" && !/^https?:\/\/[^\s;]+$/.test(source))) {
239
+ throw new Error("Invalid CSP frame-ancestors.");
240
+ }
241
+ if (sources.includes("'none'") && sources.length > 1) {
242
+ throw new Error("Invalid CSP frame-ancestors.");
243
+ }
244
+ return sources.join(" ");
245
+ };
246
+ export const createSecurityHeaders = (options = {}) => {
247
+ const headers = new Headers();
248
+ headers.set("x-content-type-options", "nosniff");
249
+ headers.set("referrer-policy", "strict-origin-when-cross-origin");
250
+ headers.set("x-frame-options", "SAMEORIGIN");
251
+ headers.set("permissions-policy", "camera=(), geolocation=(), microphone=()");
252
+ headers.set("cross-origin-opener-policy", "same-origin-allow-popups");
253
+ if (options.hsts) {
254
+ headers.set("strict-transport-security", "max-age=31536000; includeSubDomains; preload");
255
+ }
256
+ if (options.csp) {
257
+ const nonce = options.nonce ? ` 'nonce-${validateCspNonce(options.nonce)}' 'strict-dynamic'` : "";
258
+ const frameAncestors = validateFrameAncestors(options.frameAncestors ?? "'self'");
259
+ headers.set("content-security-policy", `script-src${nonce} 'report-sample'; object-src 'none'; base-uri 'none'; frame-ancestors ${frameAncestors}; form-action 'self'`);
260
+ }
261
+ return headers;
262
+ };
263
+ export const applySecurityHeaders = (response, headers) => {
264
+ const nextHeaders = new Headers(response.headers);
265
+ headers.forEach((value, key) => nextHeaders.set(key, value));
266
+ return new Response(response.body, {
267
+ status: response.status,
268
+ statusText: response.statusText,
269
+ headers: nextHeaders,
270
+ });
271
+ };
272
+ export const requireUser = (getUser, options = {}) => async ({ request, url }) => {
273
+ const user = await getUser({ request, url });
274
+ if (user) {
275
+ await options.onUser?.({ request, url, user });
276
+ return;
277
+ }
278
+ if (options.forbidden) {
279
+ return options.forbidden({ request, url });
280
+ }
281
+ return redirect(options.getRedirect?.({ request, url }) ?? options.redirectTo ?? "/login");
282
+ };
283
+ export const defineRouteModule = (module) => module;
284
+ export const routeFromModule = (id, module) => ({
285
+ id,
286
+ path: module.path ?? "/",
287
+ ...(module.loader ? { loader: module.loader } : {}),
288
+ ...(module.action ? { action: module.action } : {}),
289
+ ...(module.head ? { head: module.head } : {}),
290
+ ...(module.resources ? { resources: module.resources } : {}),
291
+ ...(module.headers ? { headers: module.headers } : {}),
292
+ ...(module.cache ? { cache: module.cache } : {}),
293
+ ...(module.fallback ? { fallback: module.fallback } : {}),
294
+ ...(module.stream ? { stream: module.stream } : {}),
295
+ ...(module.ErrorBoundary ? { error: module.ErrorBoundary } : {}),
296
+ ...(module.NotFound ? { notFound: module.NotFound } : {}),
297
+ render: module.render ?? module.template ?? (() => ""),
298
+ ...(module.children ? { children: module.children } : {}),
299
+ });
300
+ const routeSegmentFromFile = (segment) => {
301
+ if (segment === "index") {
302
+ return "";
303
+ }
304
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
305
+ return `*${segment.slice(4, -1)}`;
306
+ }
307
+ if (segment.startsWith("[") && segment.endsWith("]")) {
308
+ return `:${segment.slice(1, -1)}`;
309
+ }
310
+ return segment;
311
+ };
312
+ const idSegmentFromFile = (segment) => {
313
+ if (segment === "index") {
314
+ return "index";
315
+ }
316
+ return segment.replace(/^\[\.\.\.(.+)\]$/, "$1").replace(/^\[(.+)\]$/, "$1");
317
+ };
318
+ const routeKindForFile = (file) => {
319
+ if (/\.(?:td|tachyon(?:\.html)?)$/.test(file)) {
320
+ return "template";
321
+ }
322
+ if (/\/route\.[tj]s$/.test(file)) {
323
+ return "module";
324
+ }
325
+ if (/\/layout\.[tj]s$/.test(file)) {
326
+ return "layout";
327
+ }
328
+ return undefined;
329
+ };
330
+ const normalizeFilePath = (file) => file.replaceAll("\\", "/");
331
+ const relativeRouteFile = (file, rootDir) => {
332
+ const normalizedFile = normalizeFilePath(file);
333
+ const normalizedRoot = normalizeFilePath(rootDir).replace(/\/+$/, "");
334
+ if (normalizedFile === normalizedRoot) {
335
+ return "";
336
+ }
337
+ if (normalizedFile.startsWith(`${normalizedRoot}/`)) {
338
+ return normalizedFile.slice(normalizedRoot.length + 1);
339
+ }
340
+ return normalizedFile.replace(/^\/+/, "");
341
+ };
342
+ export const createFileRouteManifest = (files, options) => files.flatMap((file) => {
343
+ const kind = routeKindForFile(file);
344
+ if (!kind) {
345
+ return [];
346
+ }
347
+ const relative = relativeRouteFile(file, options.rootDir);
348
+ const withoutExtension = relative.replace(/\.(?:td|tachyon(?:\.html)?)$/, "").replace(/\.[tj]s$/, "");
349
+ const parts = withoutExtension.split("/");
350
+ const fileName = parts.at(-1) ?? "";
351
+ const routeParts = kind === "module" || kind === "layout" || (kind === "template" && fileName === "page")
352
+ ? parts.slice(0, -1)
353
+ : parts;
354
+ const pathSegments = routeParts.map(routeSegmentFromFile).filter(Boolean);
355
+ const routePath = pathSegments.length === 0 ? "/" : `/${pathSegments.join("/")}`;
356
+ const idParts = [
357
+ ...routeParts.map(idSegmentFromFile),
358
+ kind === "module" || kind === "layout" ? fileName : "",
359
+ ].filter(Boolean);
360
+ return [{ id: idParts.join("-") || "index", path: routePath, file, kind }];
361
+ });
362
+ export const createRouteManifest = (routes, parentPath = "", parentId) => {
363
+ const entries = [];
364
+ for (const route of routes) {
365
+ const path = joinPaths(parentPath, route.path);
366
+ const id = routeId(route, path);
367
+ entries.push({ id, path, ...(parentId ? { parentId } : {}) });
368
+ if (route.children) {
369
+ entries.push(...createRouteManifest(route.children, path, id));
370
+ }
371
+ }
372
+ return entries;
373
+ };
374
+ export const generateRouteTypes = (manifest) => {
375
+ const lines = [
376
+ `import type { ParamsForPath } from "tachyon-dom/router";`,
377
+ ``,
378
+ `export type RouteTypes = {`,
379
+ ...manifest.map((route) => ` "${route.id}": { path: "${route.path}"; params: ParamsForPath<"${route.path}"> };`),
380
+ `};`,
381
+ ``,
382
+ ];
383
+ return lines.join("\n");
384
+ };
385
+ export const createRouteBuildManifest = (routes, options) => {
386
+ const manifest = createRouteManifest(routes);
387
+ const assets = {};
388
+ for (const route of manifest) {
389
+ const definition = flattenRoutes(routes).find((candidate) => routeId(candidate.route, candidate.path) === route.id);
390
+ const staticResources = typeof definition?.route.resources === "function" ? [] : (definition?.route.resources ?? []);
391
+ assets[route.id] = staticResources.map((resource) => resource.href);
392
+ }
393
+ for (const entry of options.assets ?? []) {
394
+ assets[entry.routeId] = [...(assets[entry.routeId] ?? []), ...entry.files];
395
+ }
396
+ return {
397
+ buildId: options.buildId,
398
+ routes: manifest,
399
+ assets,
400
+ types: generateRouteTypes(manifest),
401
+ };
402
+ };
403
+ const routeById = (manifest, id) => manifest.find((route) => route.id === id);
404
+ export const hrefForRoute = (manifest, id, params = {}) => {
405
+ const route = routeById(manifest, id);
406
+ if (!route) {
407
+ throw new Error(`Unknown route id: ${id}`);
408
+ }
409
+ return route.path
410
+ .split("/")
411
+ .map((segment) => {
412
+ if (segment.startsWith(":")) {
413
+ const value = params[segment.slice(1)];
414
+ if (value === undefined) {
415
+ throw new Error(`Missing route param: ${segment.slice(1)}`);
416
+ }
417
+ return encodeURIComponent(value);
418
+ }
419
+ if (segment.startsWith("*")) {
420
+ const value = params[segment.slice(1)];
421
+ if (value === undefined) {
422
+ throw new Error(`Missing route param: ${segment.slice(1)}`);
423
+ }
424
+ return value.split("/").map(encodeURIComponent).join("/");
425
+ }
426
+ return segment;
427
+ })
428
+ .join("/");
429
+ };
430
+ export const createHrefBuilder = (manifest) => (id, params = {}) => hrefForRoute(manifest, String(id), params);
431
+ export const createRoutePreloadPlan = (manifest, routeId) => (manifest.assets[routeId] ?? []).map((href) => {
432
+ if (href.endsWith(".js") || href.endsWith(".mjs")) {
433
+ return { href, rel: "modulepreload" };
434
+ }
435
+ if (href.endsWith(".css")) {
436
+ return { href, rel: "preload", as: "style" };
437
+ }
438
+ return { href, rel: "prefetch" };
439
+ });
440
+ const compileRoutePath = (path) => {
441
+ if (path === "*") {
442
+ return { regex: /^.*$/, names: [], wildcard: true };
443
+ }
444
+ const names = [];
445
+ const segments = trimSlashes(path).split("/").filter(Boolean);
446
+ if (segments.length === 0) {
447
+ return { regex: /^\/?$/, names, wildcard: false };
448
+ }
449
+ const source = segments
450
+ .map((segment) => {
451
+ if (segment.startsWith(":")) {
452
+ names.push(segment.slice(1));
453
+ return "([^/]+)";
454
+ }
455
+ if (segment === "*" || segment.startsWith("*")) {
456
+ names.push(segment.slice(1) || "wildcard");
457
+ return "(.*)";
458
+ }
459
+ return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
460
+ })
461
+ .join("/");
462
+ return { regex: new RegExp(`^/${source}/?$`), names, wildcard: false };
463
+ };
464
+ const flattenRoutes = (routes, parentPath = "", branch = [], order = { value: 0 }) => {
465
+ const flat = [];
466
+ for (const route of routes) {
467
+ const path = joinPaths(parentPath, route.path);
468
+ const nextBranch = [...branch, { route, path }];
469
+ flat.push({ route, path, branch: nextBranch, order: order.value++ });
470
+ if (route.children) {
471
+ flat.push(...flattenRoutes(route.children, path, nextBranch, order));
472
+ }
473
+ }
474
+ return flat;
475
+ };
476
+ const routeSegmentScore = (segment) => {
477
+ if (segment === "*" || segment.startsWith("*")) {
478
+ return 0;
479
+ }
480
+ if (segment.startsWith(":")) {
481
+ return 1;
482
+ }
483
+ return 2;
484
+ };
485
+ const routeSpecificity = (path) => {
486
+ if (path === "*") {
487
+ return [-1, 0, 0];
488
+ }
489
+ const segments = trimSlashes(path).split("/").filter(Boolean);
490
+ const segmentScores = segments.map(routeSegmentScore);
491
+ return [
492
+ segmentScores.reduce((total, score) => total + score, 0),
493
+ segments.length,
494
+ segmentScores.filter((score) => score === 2).length,
495
+ ];
496
+ };
497
+ const compareCompiledRoutes = (left, right) => {
498
+ for (let index = 0; index < left.specificity.length; index += 1) {
499
+ const difference = (right.specificity[index] ?? 0) - (left.specificity[index] ?? 0);
500
+ if (difference !== 0) {
501
+ return difference;
502
+ }
503
+ }
504
+ return left.order - right.order;
505
+ };
506
+ const compiledRouteCache = new WeakMap();
507
+ const compiledRoutesFor = (routes) => {
508
+ const cached = compiledRouteCache.get(routes);
509
+ if (cached) {
510
+ return cached;
511
+ }
512
+ const compiled = flattenRoutes(routes)
513
+ .map((candidate) => {
514
+ const compiledPath = compileRoutePath(candidate.path);
515
+ return {
516
+ ...candidate,
517
+ names: compiledPath.names,
518
+ regex: compiledPath.regex,
519
+ specificity: routeSpecificity(candidate.path),
520
+ wildcard: compiledPath.wildcard,
521
+ };
522
+ })
523
+ .sort(compareCompiledRoutes);
524
+ compiledRouteCache.set(routes, compiled);
525
+ return compiled;
526
+ };
527
+ export const matchRoute = (routes, input) => {
528
+ const url = typeof input === "string" ? new URL(input, "http://tachyon.local") : input;
529
+ const pathname = url.pathname;
530
+ let fallback;
531
+ for (const candidate of compiledRoutesFor(routes)) {
532
+ const match = candidate.regex.exec(pathname);
533
+ if (!match) {
534
+ continue;
535
+ }
536
+ const params = {};
537
+ for (const [index, name] of candidate.names.entries()) {
538
+ try {
539
+ params[name] = decodeURIComponent(match[index + 1] ?? "");
540
+ }
541
+ catch {
542
+ return err(routeError(`Invalid route parameter encoding for ${name}.`, 400));
543
+ }
544
+ }
545
+ const branch = candidate.branch.map((entry) => ({ ...entry, params }));
546
+ const matched = { route: candidate.route, branch, params, pathname };
547
+ if (candidate.wildcard) {
548
+ fallback = matched;
549
+ continue;
550
+ }
551
+ return ok(matched);
552
+ }
553
+ return fallback ? ok(fallback) : err(routeError(`No route matched ${pathname}.`, 404));
554
+ };
555
+ const requestFor = (input) => {
556
+ if (input instanceof Request) {
557
+ return input;
558
+ }
559
+ return new Request(input instanceof URL ? input : new URL(input, "http://tachyon.local"));
560
+ };
561
+ const headAttributeNamePattern = /^[A-Za-z_:][A-Za-z0-9_.:-]*$/;
562
+ const urlAttributeNames = new Set(["href", "src", "action", "formaction"]);
563
+ const isSafeAttributeUrl = (value) => {
564
+ const controlCharacterPattern = /[\u0000-\u001F\u007F]/;
565
+ if (controlCharacterPattern.test(value)) {
566
+ return false;
567
+ }
568
+ const trimmed = value.trim().toLowerCase();
569
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
570
+ try {
571
+ const decoded = decodeURIComponent(trimmed);
572
+ return !decoded.startsWith("//") && !decoded.includes("\\") && !controlCharacterPattern.test(decoded);
573
+ }
574
+ catch {
575
+ return false;
576
+ }
577
+ }
578
+ if (trimmed.startsWith("#") || trimmed.startsWith("mailto:")) {
579
+ return true;
580
+ }
581
+ try {
582
+ const url = new URL(value);
583
+ return url.protocol === "https:" || url.protocol === "http:";
584
+ }
585
+ catch {
586
+ return false;
587
+ }
588
+ };
589
+ const isSafeHeadAttribute = (name, value) => {
590
+ const normalized = name.toLowerCase();
591
+ if (!headAttributeNamePattern.test(name) || normalized.startsWith("on")) {
592
+ return false;
593
+ }
594
+ return !urlAttributeNames.has(normalized) || isSafeAttributeUrl(value);
595
+ };
596
+ const hasUnsafeUrlAttribute = (attrs) => Object.entries(attrs).some(([name, value]) => {
597
+ const normalized = name.toLowerCase();
598
+ return urlAttributeNames.has(normalized) && !isSafeAttributeUrl(value);
599
+ });
600
+ const renderAttributes = (attrs, options = {}) => {
601
+ if (options.dropOnUnsafeUrl && hasUnsafeUrlAttribute(attrs)) {
602
+ return undefined;
603
+ }
604
+ return Object.entries(attrs)
605
+ .filter(([name, value]) => isSafeHeadAttribute(name, value))
606
+ .map(([name, value]) => ` ${name}="${escapeHtml(value)}"`)
607
+ .join("");
608
+ };
609
+ export const renderHead = (descriptor, options = {}) => {
610
+ const chunks = [];
611
+ if (descriptor.title !== undefined) {
612
+ chunks.push(`<title>${escapeHtml(descriptor.title)}</title>`);
613
+ }
614
+ for (const meta of descriptor.metas ?? []) {
615
+ chunks.push(`<meta${renderAttributes(meta) ?? ""}>`);
616
+ }
617
+ for (const link of descriptor.links ?? []) {
618
+ const attrs = renderAttributes(link, { dropOnUnsafeUrl: true });
619
+ if (attrs !== undefined) {
620
+ chunks.push(`<link${attrs}>`);
621
+ }
622
+ }
623
+ for (const script of descriptor.scripts ?? []) {
624
+ chunks.push(`<script${renderAttributes({ ...script, ...(options.nonce && !script.nonce ? { nonce: options.nonce } : {}) }) ?? ""}></script>`);
625
+ }
626
+ return chunks.join("");
627
+ };
628
+ export const renderResourceHints = (resources) => resources
629
+ .map((resource) => {
630
+ const attrs = { rel: resource.rel, href: resource.href };
631
+ for (const name of ["as", "type", "crossorigin", "fetchpriority"]) {
632
+ const value = resource[name];
633
+ if (value !== undefined) {
634
+ attrs[name] = value;
635
+ }
636
+ }
637
+ const rendered = renderAttributes(attrs, { dropOnUnsafeUrl: true });
638
+ return rendered === undefined ? "" : `<link${rendered}>`;
639
+ })
640
+ .join("");
641
+ export const collectRouteResources = (branch, context) => collectRouteResourcesInternal(branch, context);
642
+ const collectRouteResourcesInternal = (branch, context) => {
643
+ const resources = [];
644
+ for (const entry of branch) {
645
+ if (!entry.route.resources) {
646
+ continue;
647
+ }
648
+ const id = routeId(entry.route, entry.path);
649
+ const routeData = context?.loaderData && Object.hasOwn(context.loaderData, id) ? context.loaderData[id] : context?.data;
650
+ const value = typeof entry.route.resources === "function"
651
+ ? entry.route.resources({
652
+ request: context?.request ?? new Request("http://tachyon.local/"),
653
+ url: context?.url ?? new URL("http://tachyon.local/"),
654
+ params: context?.params ?? entry.params,
655
+ route: entry.route,
656
+ env: context?.env ?? {},
657
+ bindings: context?.bindings,
658
+ data: routeData,
659
+ loaderData: context?.loaderData ?? {},
660
+ actionResult: context?.actionResult,
661
+ outlet: context?.outlet ?? "",
662
+ })
663
+ : entry.route.resources;
664
+ resources.push(...value);
665
+ }
666
+ return resources;
667
+ };
668
+ const mergeHead = (heads) => {
669
+ const title = [...heads].reverse().find((head) => head.title !== undefined)?.title;
670
+ return {
671
+ ...(title === undefined ? {} : { title }),
672
+ metas: heads.flatMap((head) => head.metas ?? []),
673
+ links: heads.flatMap((head) => head.links ?? []),
674
+ scripts: heads.flatMap((head) => head.scripts ?? []),
675
+ };
676
+ };
677
+ const applyHeaders = (headers, extra) => {
678
+ new Headers(extra).forEach((value, key) => {
679
+ if (key === "set-cookie") {
680
+ headers.append(key, value);
681
+ return;
682
+ }
683
+ headers.set(key, value);
684
+ });
685
+ };
686
+ const ownAsyncIterable = (source) => {
687
+ const sourceIterator = source[Symbol.asyncIterator]();
688
+ let finished = false;
689
+ let returned = false;
690
+ const close = async () => {
691
+ if (finished || returned)
692
+ return;
693
+ returned = true;
694
+ await sourceIterator.return?.();
695
+ finished = true;
696
+ };
697
+ const iterator = {
698
+ [Symbol.asyncIterator]: () => iterator,
699
+ next: async () => {
700
+ if (finished)
701
+ return { done: true, value: undefined };
702
+ try {
703
+ const next = await sourceIterator.next();
704
+ if (next.done)
705
+ finished = true;
706
+ return next;
707
+ }
708
+ catch (error) {
709
+ await close();
710
+ throw error;
711
+ }
712
+ },
713
+ return: async () => {
714
+ await close();
715
+ return { done: true, value: undefined };
716
+ },
717
+ };
718
+ return iterator;
719
+ };
720
+ const nearestNotFoundBoundary = (routes, pathname) => {
721
+ const pathnameSegments = trimSlashes(pathname).split("/").filter(Boolean);
722
+ let selected;
723
+ for (const candidate of flattenRoutes(routes)) {
724
+ if (!candidate.route.notFound) {
725
+ continue;
726
+ }
727
+ const patternSegments = trimSlashes(candidate.path).split("/").filter(Boolean);
728
+ let matches = patternSegments.length <= pathnameSegments.length;
729
+ for (let index = 0; matches && index < patternSegments.length; index += 1) {
730
+ const pattern = patternSegments[index];
731
+ if (pattern === "*" || pattern.startsWith("*")) {
732
+ break;
733
+ }
734
+ if (!pattern.startsWith(":") && pattern !== pathnameSegments[index]) {
735
+ matches = false;
736
+ }
737
+ }
738
+ if (matches && (!selected || patternSegments.length > selected.depth)) {
739
+ selected = { depth: patternSegments.length, handler: candidate.route.notFound };
740
+ }
741
+ }
742
+ return selected?.handler;
743
+ };
744
+ const renderRouteInternal = async (routes, input, options = {}) => {
745
+ let request = requestFor(input);
746
+ let url = new URL(request.url);
747
+ const env = options.env ?? {};
748
+ const bindings = options.bindings;
749
+ const emptyMatch = (pathname = url.pathname) => ({
750
+ route: { path: "*", render: () => "" },
751
+ branch: [],
752
+ params: {},
753
+ pathname,
754
+ });
755
+ const routeResponseResult = (response, match = emptyMatch()) => ({
756
+ status: response.status,
757
+ html: response.headers.get("content-type")?.startsWith("text/html") ? response.body : "",
758
+ responseBody: response.body,
759
+ headHtml: "",
760
+ resourceHints: "",
761
+ stateScript: "",
762
+ loaderData: {},
763
+ actionResult: undefined,
764
+ headers: response.headers,
765
+ match,
766
+ });
767
+ const webResponseResult = async (response, match = emptyMatch()) => {
768
+ const body = await response.text();
769
+ return {
770
+ status: response.status,
771
+ html: response.headers.get("content-type")?.startsWith("text/html") ? body : "",
772
+ responseBody: body,
773
+ headHtml: "",
774
+ resourceHints: "",
775
+ stateScript: "",
776
+ loaderData: {},
777
+ actionResult: undefined,
778
+ headers: response.headers,
779
+ match,
780
+ };
781
+ };
782
+ await options.hooks?.onRequest?.({ request, url });
783
+ for (const middleware of options.middleware ?? []) {
784
+ const result = await middleware({ request, url, env, bindings });
785
+ if (isRouteResponse(result)) {
786
+ return ok(routeResponseResult(result));
787
+ }
788
+ if (isWebResponse(result)) {
789
+ return ok(await webResponseResult(result));
790
+ }
791
+ if (result instanceof Request) {
792
+ request = result;
793
+ url = new URL(request.url);
794
+ }
795
+ }
796
+ if (options.allowedMethods && !options.allowedMethods.includes(request.method)) {
797
+ return ok({
798
+ status: 405,
799
+ html: "<h1>Method Not Allowed</h1>",
800
+ headHtml: "",
801
+ resourceHints: "",
802
+ stateScript: "",
803
+ loaderData: {},
804
+ actionResult: undefined,
805
+ headers: new Headers({ allow: options.allowedMethods.join(", "), "content-type": "text/html; charset=utf-8" }),
806
+ match: emptyMatch(),
807
+ });
808
+ }
809
+ const contentLength = Number.parseInt(request.headers.get("content-length") ?? "0", 10);
810
+ if (options.maxActionBodyBytes !== undefined && contentLength > options.maxActionBodyBytes) {
811
+ return ok(payloadTooLargeResult(emptyMatch()));
812
+ }
813
+ if (options.maxActionBodyBytes !== undefined &&
814
+ request.method !== "GET" &&
815
+ request.method !== "HEAD" &&
816
+ request.body) {
817
+ const limitedRequest = await readLimitedRequest(request, options.maxActionBodyBytes);
818
+ if (!limitedRequest) {
819
+ return ok(payloadTooLargeResult(emptyMatch()));
820
+ }
821
+ request = limitedRequest;
822
+ url = new URL(request.url);
823
+ }
824
+ const match = matchRoute(routes, url);
825
+ if (!match.ok) {
826
+ const boundary = nearestNotFoundBoundary(routes, url.pathname);
827
+ const html = boundary
828
+ ? await boundary({ request, url })
829
+ : options.notFound
830
+ ? await options.notFound({ request, url })
831
+ : `<h1>Not Found</h1>`;
832
+ return ok({
833
+ status: 404,
834
+ html,
835
+ headHtml: "",
836
+ resourceHints: "",
837
+ stateScript: "",
838
+ loaderData: {},
839
+ actionResult: undefined,
840
+ headers: new Headers(),
841
+ match: emptyMatch(),
842
+ });
843
+ }
844
+ try {
845
+ await options.hooks?.onMatch?.({ request, url, match: match.value });
846
+ let actionResult;
847
+ const loaderData = {};
848
+ if (request.method !== "GET" && request.method !== "HEAD" && match.value.route.action) {
849
+ if (options.csrf && !(await verifyCsrf(request, url, env, bindings, options.csrf))) {
850
+ return ok({
851
+ status: 403,
852
+ html: "<h1>Forbidden</h1>",
853
+ headHtml: "",
854
+ resourceHints: "",
855
+ stateScript: "",
856
+ loaderData: {},
857
+ actionResult: undefined,
858
+ headers: new Headers({ "content-type": "text/html; charset=utf-8" }),
859
+ match: match.value,
860
+ });
861
+ }
862
+ actionResult = await match.value.route.action({
863
+ request,
864
+ url,
865
+ params: match.value.params,
866
+ route: match.value.route,
867
+ env,
868
+ bindings,
869
+ loaderData,
870
+ actionResult: undefined,
871
+ });
872
+ await options.hooks?.onAction?.({ request, url, route: match.value.route, result: actionResult });
873
+ if (isRouteResponse(actionResult)) {
874
+ return ok({ ...routeResponseResult(actionResult, match.value), loaderData, actionResult });
875
+ }
876
+ }
877
+ for (const entry of match.value.branch) {
878
+ const id = routeId(entry.route, entry.path);
879
+ if (entry.route.loader) {
880
+ const data = await entry.route.loader({
881
+ request,
882
+ url,
883
+ params: match.value.params,
884
+ route: entry.route,
885
+ env,
886
+ bindings,
887
+ loaderData,
888
+ actionResult,
889
+ });
890
+ if (isRouteResponse(data)) {
891
+ return ok({ ...routeResponseResult(data, match.value), loaderData, actionResult });
892
+ }
893
+ loaderData[id] = isDeferredData(data) ? await resolveDeferredData(data) : data;
894
+ await options.hooks?.onLoader?.({ request, url, route: entry.route, data: loaderData[id] });
895
+ }
896
+ }
897
+ const progressive = options.progressiveBody === true && request.method !== "HEAD" && match.value.route.stream;
898
+ let outlet = "";
899
+ const heads = [];
900
+ for (const entry of [...match.value.branch].reverse()) {
901
+ const id = routeId(entry.route, entry.path);
902
+ const data = loaderData[id];
903
+ const context = {
904
+ request,
905
+ url,
906
+ params: match.value.params,
907
+ route: entry.route,
908
+ env,
909
+ bindings,
910
+ data,
911
+ loaderData,
912
+ actionResult,
913
+ outlet,
914
+ };
915
+ if (!progressive)
916
+ outlet = await entry.route.render(context);
917
+ if (entry.route.head) {
918
+ heads.unshift(await entry.route.head(context));
919
+ }
920
+ }
921
+ outlet = applyHtmlWhitespace(outlet, options.htmlWhitespace ?? "preserve-tags");
922
+ await options.hooks?.onRender?.({ request, url, html: outlet, match: match.value });
923
+ const stateScript = Object.entries(loaderData)
924
+ .map(([id, data]) => serializeHydrationState(`route:${id}`, data, options.cspNonce === undefined ? {} : { nonce: options.cspNonce }))
925
+ .join("");
926
+ const headers = new Headers({ "content-type": "text/html; charset=utf-8" });
927
+ const deepestEntry = match.value.branch.at(-1);
928
+ const deepestContext = {
929
+ request,
930
+ url,
931
+ params: match.value.params,
932
+ route: match.value.route,
933
+ env,
934
+ bindings,
935
+ data: deepestEntry ? loaderData[routeId(deepestEntry.route, deepestEntry.path)] : undefined,
936
+ loaderData,
937
+ actionResult,
938
+ outlet,
939
+ };
940
+ for (const entry of match.value.branch) {
941
+ const id = routeId(entry.route, entry.path);
942
+ const context = { ...deepestContext, route: entry.route, data: loaderData[id] };
943
+ if (entry.route.headers) {
944
+ applyHeaders(headers, typeof entry.route.headers === "function" ? await entry.route.headers(context) : entry.route.headers);
945
+ }
946
+ if (entry.route.cache) {
947
+ const policy = typeof entry.route.cache === "function" ? await entry.route.cache(context) : entry.route.cache;
948
+ applyHeaders(headers, cacheControl(policy));
949
+ }
950
+ }
951
+ let responseChunks;
952
+ if (progressive) {
953
+ responseChunks = ownAsyncIterable(progressive(deepestContext));
954
+ }
955
+ return ok({
956
+ status: 200,
957
+ html: outlet,
958
+ headHtml: renderHead(mergeHead(heads), options.cspNonce === undefined ? {} : { nonce: options.cspNonce }),
959
+ resourceHints: renderResourceHints(collectRouteResourcesInternal(match.value.branch, {
960
+ request,
961
+ url,
962
+ params: match.value.params,
963
+ env,
964
+ bindings,
965
+ loaderData,
966
+ actionResult,
967
+ })),
968
+ stateScript,
969
+ loaderData,
970
+ actionResult,
971
+ headers,
972
+ match: match.value,
973
+ ...(responseChunks ? { responseChunks } : {}),
974
+ });
975
+ }
976
+ catch (error) {
977
+ await options.hooks?.onError?.({ request, url, error, match: match.value });
978
+ const boundary = [...match.value.branch].reverse().find((entry) => entry.route.error)?.route.error ?? options.error;
979
+ const html = boundary ? await boundary({ request, url, error }) : `<h1>Internal Server Error</h1>`;
980
+ return ok({
981
+ status: 500,
982
+ html,
983
+ headHtml: "",
984
+ resourceHints: "",
985
+ stateScript: "",
986
+ loaderData: {},
987
+ actionResult: undefined,
988
+ headers: new Headers({ "content-type": "text/html; charset=utf-8" }),
989
+ match: match.value,
990
+ });
991
+ }
992
+ };
993
+ export const renderRoute = async (routes, input, options = {}) => renderRouteInternal(routes, input, options);
994
+ /** @internal */
995
+ export const renderRouteWithBindings = renderRouteInternal;
996
+ const renderRouteStreamInternal = async (routes, input, options = {}) => {
997
+ const request = requestFor(input);
998
+ resolveHtmlWhitespacePolicy(options.htmlWhitespace ?? "preserve-tags");
999
+ const streamingOptions = { ...options, htmlWhitespace: "preserve-tags" };
1000
+ streamingOptions.progressiveBody = true;
1001
+ const rendered = await renderRouteInternal(routes, request, streamingOptions);
1002
+ if (!rendered.ok)
1003
+ return err(rendered.error);
1004
+ const body = rendered.value.responseBody ?? rendered.value.html;
1005
+ const final = {
1006
+ status: rendered.value.status,
1007
+ headHtml: rendered.value.headHtml,
1008
+ resourceHints: rendered.value.resourceHints,
1009
+ stateScript: rendered.value.stateScript,
1010
+ headers: rendered.value.headers,
1011
+ };
1012
+ return ok({
1013
+ ...final,
1014
+ chunks: rendered.value.responseChunks ??
1015
+ (async function* () {
1016
+ if (request.method !== "HEAD" && body)
1017
+ yield body;
1018
+ })(),
1019
+ final: Promise.resolve(final),
1020
+ });
1021
+ };
1022
+ export const renderRouteStream = async (routes, input, options = {}) => renderRouteStreamInternal(routes, input, options);
1023
+ /** @internal */
1024
+ export const renderRouteStreamWithBindings = renderRouteStreamInternal;