@reckona/mreact-router 0.0.141 → 0.0.142

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 (62) hide show
  1. package/README.md +4 -2
  2. package/dist/actions.d.ts +1 -1
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +28 -8
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/cloudflare.d.ts.map +1 -1
  7. package/dist/adapters/cloudflare.js +31 -12
  8. package/dist/adapters/cloudflare.js.map +1 -1
  9. package/dist/build.d.ts.map +1 -1
  10. package/dist/build.js +470 -12
  11. package/dist/build.js.map +1 -1
  12. package/dist/cli-options.js +1 -1
  13. package/dist/cli-options.js.map +1 -1
  14. package/dist/client-route-inference.d.ts +1 -1
  15. package/dist/client-route-inference.d.ts.map +1 -1
  16. package/dist/client-route-inference.js +1 -1
  17. package/dist/client-route-inference.js.map +1 -1
  18. package/dist/client.d.ts +8 -1
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +16 -0
  21. package/dist/client.js.map +1 -1
  22. package/dist/config.js +1 -1
  23. package/dist/config.js.map +1 -1
  24. package/dist/import-policy.js +7 -0
  25. package/dist/import-policy.js.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/metadata.d.ts +1 -0
  31. package/dist/metadata.d.ts.map +1 -1
  32. package/dist/metadata.js +288 -1
  33. package/dist/metadata.js.map +1 -1
  34. package/dist/middleware.d.ts +5 -0
  35. package/dist/middleware.d.ts.map +1 -1
  36. package/dist/middleware.js +13 -0
  37. package/dist/middleware.js.map +1 -1
  38. package/dist/render.d.ts.map +1 -1
  39. package/dist/render.js +41 -9
  40. package/dist/render.js.map +1 -1
  41. package/dist/typed-routes.d.ts +24 -0
  42. package/dist/typed-routes.d.ts.map +1 -0
  43. package/dist/typed-routes.js +60 -0
  44. package/dist/typed-routes.js.map +1 -0
  45. package/dist/vite.d.ts.map +1 -1
  46. package/dist/vite.js +22 -1
  47. package/dist/vite.js.map +1 -1
  48. package/package.json +11 -11
  49. package/src/actions.ts +39 -12
  50. package/src/adapters/cloudflare.ts +48 -12
  51. package/src/build.ts +491 -11
  52. package/src/cli-options.ts +1 -1
  53. package/src/client-route-inference.ts +1 -0
  54. package/src/client.ts +28 -1
  55. package/src/config.ts +1 -1
  56. package/src/import-policy.ts +8 -0
  57. package/src/index.ts +9 -0
  58. package/src/metadata.ts +388 -1
  59. package/src/middleware.ts +22 -0
  60. package/src/render.ts +58 -7
  61. package/src/typed-routes.ts +121 -0
  62. package/src/vite.ts +25 -0
package/dist/build.js CHANGED
@@ -6,7 +6,7 @@ import { dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { collectStaticImportReferences, collectTopLevelValueExportNames, formatDiagnostic, hasModuleDirective, transform, } from "@reckona/mreact-compiler";
8
8
  import { transformCompilerModuleContext } from "@reckona/mreact-compiler/internal";
9
- import { compilerModuleContextForSource, collectClientRouteReferences, createClientRouteInferenceCache, detectClientNavigationHint, formatClientRouteInferenceDiagnostic, inferClientRouteModule, resolveNavigationRuntime, } from "./client-route-inference.js";
9
+ import { compilerModuleContextForSource, collectClientRouteReferences, createClientRouteInferenceCache, detectClientNavigationHint, formatClientRouteInferenceDiagnostic, inferClientRouteModule, navigationRuntimeLinkDisabledDiagnostic, resolveNavigationRuntime, } from "./client-route-inference.js";
10
10
  import { buildClientRouteBatchOutput, buildNavigationRuntimeBundle, clientScriptForPath, routeIdForPath, } from "./navigation-runtime.js";
11
11
  import { bundleAppRouterSourceModule, fileImportMetaUrlPlugin, importAppRouterSourceModule, } from "./module-runner.js";
12
12
  import { scanAppRoutes } from "./routes.js";
@@ -23,12 +23,43 @@ import { sourceModuleCandidates } from "./source-modules.js";
23
23
  import { collectBuildInferredServerActions } from "./server-action-inference.js";
24
24
  import { viteDefineCacheKey, vitePluginsCacheKey } from "./vite-plugin-cache-key.js";
25
25
  import { workspacePackageFile } from "./workspace-packages.js";
26
+ import { parseRouteMiddlewareControl, parseStaticMiddlewareConfig, validateRouteMiddlewareControl, } from "./middleware.js";
26
27
  const nativeEscapeTransform = {
27
28
  batchImportName: "escapeHtmlBatch",
28
29
  batchImportSource: "@reckona/mreact-router/native-escape",
29
30
  };
30
31
  const defaultBuildConcurrency = Math.max(1, Math.min(2, availableParallelism()));
31
32
  const serverArtifactFilesystemConcurrency = 2;
33
+ async function validateBuildMiddlewareControls(appDir, routes) {
34
+ const availableIds = await collectBuildMiddlewareIds(appDir);
35
+ for (const route of routes) {
36
+ if (route.kind !== "page") {
37
+ continue;
38
+ }
39
+ validateRouteMiddlewareControl({
40
+ availableIds,
41
+ control: parseRouteMiddlewareControl(await readFile(route.file, "utf8")),
42
+ routePath: route.path,
43
+ });
44
+ }
45
+ }
46
+ async function collectBuildMiddlewareIds(appDir) {
47
+ const ids = new Set();
48
+ for (const file of [join(appDir, "middleware.ts"), join(appDir, "middleware.mreact.ts")]) {
49
+ let code;
50
+ try {
51
+ code = await readFile(file, "utf8");
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ const id = parseStaticMiddlewareConfig(code).id;
57
+ if (id !== undefined) {
58
+ ids.add(id);
59
+ }
60
+ }
61
+ return ids;
62
+ }
32
63
  export async function buildApp(options) {
33
64
  const timingSink = options.onBuildPhaseTiming;
34
65
  const project = resolveAppRouterProjectOptions(options);
@@ -38,6 +69,7 @@ export async function buildApp(options) {
38
69
  const routes = timingSink === undefined
39
70
  ? await scanAppRoutes({ appDir: project.routesDir })
40
71
  : await timeBuildPhase(timingSink, "scan", () => scanAppRoutes({ appDir: project.routesDir }));
72
+ await validateBuildMiddlewareControls(project.routesDir, routes);
41
73
  const viteDefine = options.viteConfig?.define;
42
74
  const vitePlugins = options.viteConfig?.plugins;
43
75
  const files = timingSink === undefined
@@ -311,6 +343,7 @@ export async function buildApp(options) {
311
343
  const writeManifestFiles = async () => {
312
344
  await Promise.all([
313
345
  writeFile(join(serverDir, "manifest.json"), JSON.stringify(serverManifest, null, 2)),
346
+ writeFile(join(options.outDir, "routes.d.ts"), typedRoutesDeclaration(routes)),
314
347
  writeFile(join(serverDir, "import-policy.json"), JSON.stringify(generatedImportPolicy, null, 2)),
315
348
  writeFile(join(clientDir, "manifest.json"), JSON.stringify(clientManifest, null, 2)),
316
349
  writeFile(join(clientDir, ".vite", "manifest.json"), JSON.stringify(viteManifestFromClientRoutes(clientManifestRoutes), null, 2)),
@@ -356,6 +389,32 @@ export async function buildApp(options) {
356
389
  }
357
390
  return { routes };
358
391
  }
392
+ function typedRoutesDeclaration(routes) {
393
+ const routePaths = Array.from(new Set(routes
394
+ .filter((route) => route.kind === "page" || route.kind === "server")
395
+ .map((route) => route.path))).sort((left, right) => {
396
+ if (left === "/") {
397
+ return -1;
398
+ }
399
+ if (right === "/") {
400
+ return 1;
401
+ }
402
+ return left.localeCompare(right);
403
+ });
404
+ const routeUnion = routePaths.map((routePath) => JSON.stringify(routePath)).join(" | ");
405
+ const routesObject = routePaths
406
+ .map((routePath) => ` readonly ${JSON.stringify(routePath)}: AppRouteHref<${JSON.stringify(routePath)}>;`)
407
+ .join("\n");
408
+ return [
409
+ `import type { AppRouteHref } from "@reckona/mreact-router";`,
410
+ ``,
411
+ `export type AppRoutePath = ${routeUnion === "" ? "never" : routeUnion};`,
412
+ `export declare const routes: {`,
413
+ routesObject,
414
+ `};`,
415
+ ``,
416
+ ].join("\n");
417
+ }
359
418
  async function timeBuildPhase(sink, phase, run) {
360
419
  const startedAt = performance.now();
361
420
  try {
@@ -1807,6 +1866,8 @@ function cloudflarePageRouteFacadeModuleSource(componentImport) {
1807
1866
  return `import * as componentModule from ${JSON.stringify(componentImport)};
1808
1867
 
1809
1868
  const componentSlots = readComponentModuleExport(componentModule, "slots");
1869
+ const componentGenerateMetadata = readComponentModuleExport(componentModule, "generateMetadata");
1870
+ const componentMetadata = readComponentModuleExport(componentModule, "metadata");
1810
1871
 
1811
1872
  export function App(props) {
1812
1873
  return renderCloudflareRouteComponent(props);
@@ -1818,6 +1879,9 @@ export function CloudflareRouteComponent(props) {
1818
1879
  return renderCloudflareRouteComponent(props);
1819
1880
  }
1820
1881
  export const slots = componentSlots === undefined ? undefined : { ...componentSlots };
1882
+ export const generateMetadata =
1883
+ typeof componentGenerateMetadata === "function" ? componentGenerateMetadata : undefined;
1884
+ export const metadata = componentMetadata;
1821
1885
 
1822
1886
  function renderCloudflareRouteComponent(props) {
1823
1887
  const routeComponent = resolveCloudflareRouteComponent();
@@ -2098,9 +2162,9 @@ async function renderCloudflareStringRoute(props) {
2098
2162
  }
2099
2163
  html = injectCloudflareHead(html, metadata, cloudflareRouteHeadTags(props.clientManifest, props.route.path));
2100
2164
  return new Response(html, {
2101
- headers: {
2165
+ headers: cloudflareMetadataHeaders(metadata, props.request, {
2102
2166
  "content-type": "text/html; charset=utf-8"
2103
- }
2167
+ })
2104
2168
  });
2105
2169
  }
2106
2170
 
@@ -2238,9 +2302,9 @@ async function renderCloudflareStringRoute(props) {
2238
2302
  }
2239
2303
  html = injectCloudflareHead(html, metadata, cloudflareRouteHeadTags(props.clientManifest, props.route.path));
2240
2304
  return new Response(html, {
2241
- headers: {
2305
+ headers: cloudflareMetadataHeaders(metadata, props.request, {
2242
2306
  "content-type": "text/html; charset=utf-8"
2243
- }
2307
+ })
2244
2308
  });
2245
2309
  }
2246
2310
 
@@ -2313,11 +2377,11 @@ export const slots = pageModule.slots;
2313
2377
  export const App = renderCloudflareStreamRoute;
2314
2378
  export default renderCloudflareStreamRoute;
2315
2379
 
2316
- function renderCloudflareStreamRoute(props) {
2380
+ async function renderCloudflareStreamRoute(props) {
2381
+ const metadata = await resolveRouteMetadata([...shells.map((shell) => shell.module), pageModule], props);
2317
2382
  const body = renderToReadableStream(async ($sink) => {
2318
2383
  const slotHtml = await renderRouteSlots(pageModule.slots, props);
2319
2384
  const layoutShells = await renderLayoutShells(shells, props, slotHtml);
2320
- const metadata = await resolveRouteMetadata([...shells.map((shell) => shell.module), pageModule], props);
2321
2385
  const routeHeadTags = cloudflareRouteHeadTags(props.clientManifest, props.route.path);
2322
2386
  $sink.append("<!DOCTYPE html>");
2323
2387
  if (layoutShells.length === 0) {
@@ -2337,10 +2401,10 @@ function renderCloudflareStreamRoute(props) {
2337
2401
  }
2338
2402
  });
2339
2403
  return new Response(body, {
2340
- headers: {
2404
+ headers: cloudflareMetadataHeaders(metadata, props.request, {
2341
2405
  "content-type": "text/html; charset=utf-8",
2342
2406
  "x-mreact-stream": "1"
2343
- }
2407
+ })
2344
2408
  });
2345
2409
  }
2346
2410
 
@@ -2513,16 +2577,401 @@ async function resolveRouteMetadata(modules, props) {
2513
2577
  request: props.request
2514
2578
  };
2515
2579
  for (const module of modules) {
2516
- let next = module.metadata;
2580
+ let next = validateRouteMetadata(module.metadata);
2517
2581
  if (typeof module.generateMetadata === "function") {
2518
- const generated = await module.generateMetadata(context);
2582
+ const generated = validateRouteMetadata(await module.generateMetadata(context), "generateMetadata");
2519
2583
  next = mergeRouteMetadata([next, generated].filter(Boolean));
2520
2584
  }
2521
2585
  if (next !== undefined) {
2522
2586
  metadata.push(next);
2523
2587
  }
2524
2588
  }
2525
- return mergeRouteMetadata(metadata);
2589
+ return validateRouteMetadata(mergeRouteMetadata(metadata));
2590
+ }
2591
+
2592
+ function validateRouteMetadata(metadata, path = "metadata") {
2593
+ if (metadata === undefined) {
2594
+ return undefined;
2595
+ }
2596
+ assertMetadataObject(metadata, path);
2597
+ validateOptionalMetadataObject(metadata.alternates, \`\${path}.alternates\`, { canonical: validateMetadataScalar });
2598
+ validateOptionalCspMetadata(metadata.csp, \`\${path}.csp\`);
2599
+ validateOptionalMetadataScalar(metadata.description, \`\${path}.description\`);
2600
+ validateOptionalHeadMetadata(metadata.head, \`\${path}.head\`);
2601
+ validateOptionalMetadataObject(metadata.icons, \`\${path}.icons\`, {
2602
+ apple: validateMetadataScalar,
2603
+ icon: validateMetadataScalar,
2604
+ });
2605
+ validateOptionalOpenGraphMetadata(metadata.openGraph, \`\${path}.openGraph\`);
2606
+ validateOptionalMetadataScalar(metadata.lang, \`\${path}.lang\`);
2607
+ validateOptionalRobotsMetadata(metadata.robots, \`\${path}.robots\`);
2608
+ validateOptionalSecurityMetadata(metadata.security, \`\${path}.security\`);
2609
+ validateOptionalThemeColorMetadata(metadata.themeColor, \`\${path}.themeColor\`);
2610
+ validateOptionalMetadataScalar(metadata.title, \`\${path}.title\`);
2611
+ validateOptionalViewportMetadata(metadata.viewport, \`\${path}.viewport\`);
2612
+ validateUnknownJsonMetadataFields(metadata, path, new Set(["alternates", "csp", "description", "head", "icons", "lang", "openGraph", "robots", "security", "themeColor", "title", "viewport"]));
2613
+ return metadata;
2614
+ }
2615
+
2616
+ function validateOptionalMetadataScalar(value, path) {
2617
+ if (value !== undefined) {
2618
+ validateMetadataScalar(value, path);
2619
+ }
2620
+ }
2621
+
2622
+ function validateMetadataScalar(value, path) {
2623
+ if (!isMetadataScalar(value)) {
2624
+ throw new Error(\`Invalid metadata field \${path}: expected string, number, or boolean.\`);
2625
+ }
2626
+ }
2627
+
2628
+ function isMetadataScalar(value) {
2629
+ return typeof value === "string" || (typeof value === "number" && Number.isFinite(value)) || typeof value === "boolean";
2630
+ }
2631
+
2632
+ function validateOptionalMetadataObject(value, path, validators) {
2633
+ if (value === undefined) {
2634
+ return;
2635
+ }
2636
+ assertMetadataObject(value, path);
2637
+ for (const [key, validator] of Object.entries(validators)) {
2638
+ if (value[key] !== undefined) {
2639
+ validator(value[key], \`\${path}.\${key}\`);
2640
+ }
2641
+ }
2642
+ validateUnknownJsonMetadataFields(value, path, new Set(Object.keys(validators)));
2643
+ }
2644
+
2645
+ function validateOptionalCspMetadata(value, path) {
2646
+ if (value === undefined) {
2647
+ return;
2648
+ }
2649
+ assertMetadataObject(value, path);
2650
+ if (value.disable !== undefined && typeof value.disable !== "boolean") {
2651
+ throw new Error(\`Invalid metadata field \${path}.disable: expected boolean.\`);
2652
+ }
2653
+ if (value.nonce !== undefined && typeof value.nonce !== "string") {
2654
+ throw new Error(\`Invalid metadata field \${path}.nonce: expected string.\`);
2655
+ }
2656
+ validateOptionalDirectiveMap(value.directives, \`\${path}.directives\`);
2657
+ validateOptionalDirectiveMap(value.replace, \`\${path}.replace\`);
2658
+ if (value.remove !== undefined) {
2659
+ validateStringArray(value.remove, \`\${path}.remove\`);
2660
+ }
2661
+ validateUnknownJsonMetadataFields(value, path, new Set(["directives", "disable", "nonce", "remove", "replace"]));
2662
+ }
2663
+
2664
+ function validateOptionalDirectiveMap(value, path) {
2665
+ if (value === undefined) {
2666
+ return;
2667
+ }
2668
+ assertMetadataObject(value, path);
2669
+ for (const [name, directive] of Object.entries(value)) {
2670
+ if (typeof directive !== "string") {
2671
+ validateStringArray(directive, \`\${path}.\${name}\`);
2672
+ }
2673
+ }
2674
+ }
2675
+
2676
+ function validateStringArray(value, path) {
2677
+ if (!Array.isArray(value)) {
2678
+ throw new Error(\`Invalid metadata field \${path}: expected string or string array.\`);
2679
+ }
2680
+ value.forEach((entry, index) => {
2681
+ if (typeof entry !== "string") {
2682
+ throw new Error(\`Invalid metadata field \${path}.\${index}: expected string.\`);
2683
+ }
2684
+ });
2685
+ }
2686
+
2687
+ function validateOptionalHeadMetadata(value, path) {
2688
+ if (value === undefined) {
2689
+ return;
2690
+ }
2691
+ if (!Array.isArray(value)) {
2692
+ throw new Error(\`Invalid metadata field \${path}: expected array.\`);
2693
+ }
2694
+ value.forEach((descriptor, index) => {
2695
+ const descriptorPath = \`\${path}.\${index}\`;
2696
+ assertMetadataObject(descriptor, descriptorPath);
2697
+ if (!["base", "link", "meta", "script", "style"].includes(String(descriptor.tag))) {
2698
+ throw new Error(\`Invalid metadata field \${descriptorPath}.tag: expected supported head tag.\`);
2699
+ }
2700
+ if (descriptor.content !== undefined && typeof descriptor.content !== "string") {
2701
+ throw new Error(\`Invalid metadata field \${descriptorPath}.content: expected string.\`);
2702
+ }
2703
+ if (descriptor.nonce !== undefined && typeof descriptor.nonce !== "boolean" && typeof descriptor.nonce !== "string") {
2704
+ throw new Error(\`Invalid metadata field \${descriptorPath}.nonce: expected string or boolean.\`);
2705
+ }
2706
+ if (descriptor.attrs !== undefined) {
2707
+ assertMetadataObject(descriptor.attrs, \`\${descriptorPath}.attrs\`);
2708
+ for (const [name, attr] of Object.entries(descriptor.attrs)) {
2709
+ validateHeadAttribute(name, attr, \`\${descriptorPath}.attrs.\${name}\`);
2710
+ if (attr !== undefined && typeof attr !== "boolean" && typeof attr !== "number" && typeof attr !== "string") {
2711
+ throw new Error(\`Invalid metadata field \${descriptorPath}.attrs.\${name}: expected string, number, boolean, or undefined.\`);
2712
+ }
2713
+ }
2714
+ }
2715
+ });
2716
+ }
2717
+
2718
+ function validateHeadAttribute(name, value, path) {
2719
+ if (!isSafeHeadAttributeName(name)) {
2720
+ throw new Error(\`Invalid metadata field \${path}: expected safe HTML attribute name.\`);
2721
+ }
2722
+ const canonicalName = name.toLowerCase();
2723
+ if (canonicalName.startsWith("on") || canonicalName === "srcdoc") {
2724
+ throw new Error(\`Invalid metadata field \${path}: event and dangerous attributes are not allowed.\`);
2725
+ }
2726
+ if (typeof value === "string" && isUnsafeUrlAttribute(canonicalName, value)) {
2727
+ throw new Error(\`Invalid metadata field \${path}: unsafe URL value.\`);
2728
+ }
2729
+ }
2730
+
2731
+ function isSafeHeadAttributeName(name) {
2732
+ if (name.length === 0) {
2733
+ return false;
2734
+ }
2735
+ for (let index = 0; index < name.length; index += 1) {
2736
+ const code = name.charCodeAt(index);
2737
+ if (code <= 0x20 || code === 0x22 || code === 0x27 || code === 0x2f || code === 0x3c || code === 0x3d || code === 0x3e || code === 0x60 || code === 0x7f) {
2738
+ return false;
2739
+ }
2740
+ }
2741
+ return true;
2742
+ }
2743
+
2744
+ function isUnsafeUrlAttribute(name, value) {
2745
+ if (name === "srcset" || name === "imagesrcset") {
2746
+ const canonical = canonicalizeUrlForSchemeCheck(value);
2747
+ for (const candidate of canonical.split(",")) {
2748
+ const url = candidate.trim().split(/\\s+/)[0] ?? "";
2749
+ if (url !== "" && isUnsafeUrlValueForName("src", url)) {
2750
+ return true;
2751
+ }
2752
+ }
2753
+ return false;
2754
+ }
2755
+ if (name !== "href" && name !== "src" && name !== "action" && name !== "formaction" && name !== "xlink:href" && name !== "ping" && name !== "poster" && name !== "background" && name !== "manifest") {
2756
+ return false;
2757
+ }
2758
+
2759
+ return isUnsafeUrlValueForName(name, value);
2760
+ }
2761
+
2762
+ function canonicalizeUrlForSchemeCheck(value) {
2763
+ let start = 0;
2764
+ while (start < value.length && value.charCodeAt(start) <= 0x20) {
2765
+ start += 1;
2766
+ }
2767
+
2768
+ return value.slice(start).replace(/[\\t\\r\\n]/g, "");
2769
+ }
2770
+
2771
+ function isUnsafeUrlValueForName(name, value) {
2772
+ const canonical = canonicalizeUrlForSchemeCheck(value);
2773
+ const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(canonical);
2774
+ if (match === null) {
2775
+ return false;
2776
+ }
2777
+ const scheme = match[1].toLowerCase();
2778
+ if (scheme === "data" && (name === "src" || name === "poster")) {
2779
+ return !/^data:image\\/(?!svg\\+xml(?:[;,]|$))/i.test(canonical);
2780
+ }
2781
+ return scheme === "javascript" || scheme === "data" || scheme === "vbscript" || scheme === "livescript" || scheme === "mhtml" || scheme === "file";
2782
+ }
2783
+
2784
+ function validateOptionalOpenGraphMetadata(value, path) {
2785
+ if (value === undefined) {
2786
+ return;
2787
+ }
2788
+ assertMetadataObject(value, path);
2789
+ validateOptionalMetadataScalar(value.description, \`\${path}.description\`);
2790
+ validateOptionalMetadataImage(value.image, \`\${path}.image\`);
2791
+ if (value.images !== undefined) {
2792
+ if (!Array.isArray(value.images)) {
2793
+ throw new Error(\`Invalid metadata field \${path}.images: expected array.\`);
2794
+ }
2795
+ value.images.forEach((image, index) => validateOptionalMetadataImage(image, \`\${path}.images.\${index}\`));
2796
+ }
2797
+ validateOptionalMetadataScalar(value.title, \`\${path}.title\`);
2798
+ validateUnknownJsonMetadataFields(value, path, new Set(["description", "image", "images", "title"]));
2799
+ }
2800
+
2801
+ function validateOptionalMetadataImage(value, path) {
2802
+ if (value === undefined || isMetadataScalar(value)) {
2803
+ return;
2804
+ }
2805
+ assertMetadataObject(value, path);
2806
+ validateMetadataScalar(value.url, \`\${path}.url\`);
2807
+ validateOptionalMetadataScalar(value.alt, \`\${path}.alt\`);
2808
+ validateOptionalMetadataScalar(value.height, \`\${path}.height\`);
2809
+ validateOptionalMetadataScalar(value.type, \`\${path}.type\`);
2810
+ validateOptionalMetadataScalar(value.width, \`\${path}.width\`);
2811
+ validateUnknownJsonMetadataFields(value, path, new Set(["alt", "height", "type", "url", "width"]));
2812
+ }
2813
+
2814
+ function validateOptionalRobotsMetadata(value, path) {
2815
+ if (value === undefined || typeof value === "string") {
2816
+ return;
2817
+ }
2818
+ assertMetadataObject(value, path);
2819
+ if (value.follow !== undefined && typeof value.follow !== "boolean") {
2820
+ throw new Error(\`Invalid metadata field \${path}.follow: expected boolean.\`);
2821
+ }
2822
+ if (value.index !== undefined && typeof value.index !== "boolean") {
2823
+ throw new Error(\`Invalid metadata field \${path}.index: expected boolean.\`);
2824
+ }
2825
+ validateUnknownJsonMetadataFields(value, path, new Set(["follow", "index"]));
2826
+ }
2827
+
2828
+ function validateOptionalSecurityMetadata(value, path) {
2829
+ if (value !== undefined) {
2830
+ validateJsonSerializableMetadata(value, path);
2831
+ }
2832
+ }
2833
+
2834
+ function validateOptionalThemeColorMetadata(value, path) {
2835
+ if (value === undefined || isMetadataScalar(value)) {
2836
+ return;
2837
+ }
2838
+ validateOptionalMetadataObject(value, path, {
2839
+ color: validateMetadataScalar,
2840
+ media: validateMetadataScalar,
2841
+ });
2842
+ }
2843
+
2844
+ function validateOptionalViewportMetadata(value, path) {
2845
+ if (value === undefined || isMetadataScalar(value)) {
2846
+ return;
2847
+ }
2848
+ assertMetadataObject(value, path);
2849
+ for (const [key, viewportValue] of Object.entries(value)) {
2850
+ if (viewportValue !== undefined && viewportValue !== null && !isMetadataScalar(viewportValue)) {
2851
+ throw new Error(\`Invalid metadata field \${path}.\${key}: expected string, number, boolean, null, or undefined.\`);
2852
+ }
2853
+ }
2854
+ }
2855
+
2856
+ function validateUnknownJsonMetadataFields(value, path, knownFields) {
2857
+ for (const [key, entry] of Object.entries(value)) {
2858
+ if (!knownFields.has(key)) {
2859
+ validateJsonSerializableMetadata(entry, \`\${path}.\${key}\`);
2860
+ }
2861
+ }
2862
+ }
2863
+
2864
+ function validateJsonSerializableMetadata(value, path) {
2865
+ if (value === undefined || value === null || isMetadataScalar(value)) {
2866
+ return;
2867
+ }
2868
+ if (Array.isArray(value)) {
2869
+ value.forEach((entry, index) => validateJsonSerializableMetadata(entry, \`\${path}.\${index}\`));
2870
+ return;
2871
+ }
2872
+ if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
2873
+ for (const [key, entry] of Object.entries(value)) {
2874
+ validateJsonSerializableMetadata(entry, \`\${path}.\${key}\`);
2875
+ }
2876
+ return;
2877
+ }
2878
+ throw new Error(\`Invalid metadata field \${path}: expected a JSON-serializable value.\`);
2879
+ }
2880
+
2881
+ function assertMetadataObject(value, path) {
2882
+ if (typeof value !== "object" || value === null || Array.isArray(value) || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
2883
+ throw new Error(\`Invalid metadata field \${path}: expected object.\`);
2884
+ }
2885
+ }
2886
+
2887
+ function cloudflareMetadataHeaders(metadata, request, extraHeaders) {
2888
+ const headers = new Headers(extraHeaders);
2889
+ const csp = contentSecurityPolicy(metadata?.csp);
2890
+ if (csp !== undefined && !headers.has("content-security-policy")) {
2891
+ headers.set("content-security-policy", csp);
2892
+ }
2893
+ for (const [name, value] of Object.entries(routeSecurityHeaders(metadata?.security, request))) {
2894
+ if (!headers.has(name)) {
2895
+ headers.set(name, value);
2896
+ }
2897
+ }
2898
+ return headers;
2899
+ }
2900
+
2901
+ function contentSecurityPolicy(csp) {
2902
+ if (csp?.disable === true || csp?.directives === undefined) {
2903
+ return undefined;
2904
+ }
2905
+ const serialized = [];
2906
+ for (const [name, value] of Object.entries(csp.directives)) {
2907
+ if (!/^[a-z][a-z0-9-]*$/i.test(name)) {
2908
+ throw new TypeError(\`invalid CSP directive name: \${JSON.stringify(name)}\`);
2909
+ }
2910
+ const values = Array.isArray(value) ? [...value] : [value];
2911
+ for (const rawValue of values) {
2912
+ if (typeof rawValue !== "string" || !isValidCspDirectiveValue(rawValue)) {
2913
+ throw new TypeError(\`invalid CSP directive value for \${name}: \${JSON.stringify(rawValue)}\`);
2914
+ }
2915
+ }
2916
+ if (csp.nonce !== undefined && (name === "script-src" || name === "style-src")) {
2917
+ values.push(\`'nonce-\${csp.nonce}'\`);
2918
+ }
2919
+ serialized.push(\`\${name} \${values.join(" ")}\`);
2920
+ }
2921
+ return serialized.join("; ");
2922
+ }
2923
+
2924
+ function isValidCspDirectiveValue(value) {
2925
+ if (/^'[A-Za-z0-9+/=_:.-]+'$/.test(value)) {
2926
+ return true;
2927
+ }
2928
+ if (value.length === 0) {
2929
+ return false;
2930
+ }
2931
+ for (let index = 0; index < value.length; index += 1) {
2932
+ const code = value.charCodeAt(index);
2933
+ if (code <= 0x20 || code === 0x22 || code === 0x27 || code === 0x3b || code === 0x7f) {
2934
+ return false;
2935
+ }
2936
+ }
2937
+ return true;
2938
+ }
2939
+
2940
+ function routeSecurityHeaders(security, request) {
2941
+ const headers = {
2942
+ "permissions-policy": "camera=(), microphone=(), geolocation=()",
2943
+ "referrer-policy": "strict-origin-when-cross-origin",
2944
+ "x-content-type-options": "nosniff",
2945
+ };
2946
+ if (security?.contentTypeOptions === null) {
2947
+ delete headers["x-content-type-options"];
2948
+ } else {
2949
+ headers["x-content-type-options"] = validateHeaderValue(security?.contentTypeOptions ?? "nosniff");
2950
+ }
2951
+ if (security?.referrerPolicy === null) {
2952
+ delete headers["referrer-policy"];
2953
+ } else {
2954
+ headers["referrer-policy"] = validateHeaderValue(security?.referrerPolicy ?? "strict-origin-when-cross-origin");
2955
+ }
2956
+ if (security?.frameOptions === null) {
2957
+ delete headers["x-frame-options"];
2958
+ } else if (security?.frameOptions !== undefined) {
2959
+ headers["x-frame-options"] = validateHeaderValue(security.frameOptions);
2960
+ }
2961
+ if (request.url.startsWith("https://") && security?.hsts !== undefined && security.hsts !== false && security.hsts !== null) {
2962
+ headers["strict-transport-security"] = \`max-age=\${Math.trunc(security.hsts.maxAge)}\${security.hsts.includeSubDomains === true ? "; includeSubDomains" : ""}\${security.hsts.preload === true ? "; preload" : ""}\`;
2963
+ }
2964
+ return headers;
2965
+ }
2966
+
2967
+ function validateHeaderValue(value) {
2968
+ for (let index = 0; index < value.length; index += 1) {
2969
+ const code = value.charCodeAt(index);
2970
+ if (code <= 0x1f || code === 0x7f) {
2971
+ throw new TypeError(\`Invalid security header value: \${JSON.stringify(value)}\`);
2972
+ }
2973
+ }
2974
+ return value;
2526
2975
  }
2527
2976
 
2528
2977
  function mergeRouteMetadata(metadata) {
@@ -3186,6 +3635,15 @@ async function writeClientRouteBundles(options) {
3186
3635
  for (const diagnostic of references.diagnostics) {
3187
3636
  console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
3188
3637
  }
3638
+ const navigationRuntimeDiagnostic = navigationRuntimeLinkDisabledDiagnostic({
3639
+ filename: route.file,
3640
+ references,
3641
+ routePath: route.path,
3642
+ source,
3643
+ });
3644
+ if (navigationRuntimeDiagnostic !== undefined) {
3645
+ console.warn(formatClientRouteInferenceDiagnostic(navigationRuntimeDiagnostic));
3646
+ }
3189
3647
  if (!references.client) {
3190
3648
  return {
3191
3649
  manifest: {