@reckona/mreact-router 0.0.81 → 0.0.83

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 (81) hide show
  1. package/README.md +2 -0
  2. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  3. package/dist/adapters/aws-lambda.js +17 -22
  4. package/dist/adapters/aws-lambda.js.map +1 -1
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +3 -1
  7. package/dist/build.js.map +1 -1
  8. package/dist/client-route-inference.d.ts +2 -0
  9. package/dist/client-route-inference.d.ts.map +1 -0
  10. package/dist/client-route-inference.js +2 -0
  11. package/dist/client-route-inference.js.map +1 -0
  12. package/dist/client.d.ts +2 -1
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +89 -24
  15. package/dist/client.js.map +1 -1
  16. package/dist/layout-composer.d.ts +24 -0
  17. package/dist/layout-composer.d.ts.map +1 -0
  18. package/dist/layout-composer.js +83 -0
  19. package/dist/layout-composer.js.map +1 -0
  20. package/dist/metadata.d.ts +9 -0
  21. package/dist/metadata.d.ts.map +1 -0
  22. package/dist/metadata.js +419 -0
  23. package/dist/metadata.js.map +1 -0
  24. package/dist/middleware.d.ts +22 -0
  25. package/dist/middleware.d.ts.map +1 -0
  26. package/dist/middleware.js +94 -0
  27. package/dist/middleware.js.map +1 -0
  28. package/dist/module-runner.d.ts +2 -2
  29. package/dist/module-runner.d.ts.map +1 -1
  30. package/dist/module-runner.js +1 -1
  31. package/dist/module-runner.js.map +1 -1
  32. package/dist/navigation-runtime.d.ts +2 -0
  33. package/dist/navigation-runtime.d.ts.map +1 -0
  34. package/dist/navigation-runtime.js +2 -0
  35. package/dist/navigation-runtime.js.map +1 -0
  36. package/dist/preload-policy.d.ts +24 -0
  37. package/dist/preload-policy.d.ts.map +1 -0
  38. package/dist/preload-policy.js +45 -0
  39. package/dist/preload-policy.js.map +1 -0
  40. package/dist/render.d.ts +1 -3
  41. package/dist/render.d.ts.map +1 -1
  42. package/dist/render.js +32 -676
  43. package/dist/render.js.map +1 -1
  44. package/dist/route-hydration-contract.d.ts +11 -0
  45. package/dist/route-hydration-contract.d.ts.map +1 -0
  46. package/dist/route-hydration-contract.js +21 -0
  47. package/dist/route-hydration-contract.js.map +1 -0
  48. package/dist/route-module-loader.d.ts +10 -0
  49. package/dist/route-module-loader.d.ts.map +1 -0
  50. package/dist/route-module-loader.js +61 -0
  51. package/dist/route-module-loader.js.map +1 -0
  52. package/dist/routes.js +6 -0
  53. package/dist/routes.js.map +1 -1
  54. package/dist/serve.d.ts +13 -0
  55. package/dist/serve.d.ts.map +1 -1
  56. package/dist/serve.js +63 -28
  57. package/dist/serve.js.map +1 -1
  58. package/dist/server-action-inference.d.ts.map +1 -1
  59. package/dist/server-action-inference.js +37 -1
  60. package/dist/server-action-inference.js.map +1 -1
  61. package/dist/vite.d.ts.map +1 -1
  62. package/dist/vite.js +2 -1
  63. package/dist/vite.js.map +1 -1
  64. package/package.json +11 -11
  65. package/src/adapters/aws-lambda.ts +21 -28
  66. package/src/build.ts +9 -6
  67. package/src/client-route-inference.ts +18 -0
  68. package/src/client.ts +124 -24
  69. package/src/layout-composer.ts +142 -0
  70. package/src/metadata.ts +578 -0
  71. package/src/middleware.ts +153 -0
  72. package/src/module-runner.ts +3 -2
  73. package/src/navigation-runtime.ts +16 -0
  74. package/src/preload-policy.ts +89 -0
  75. package/src/render.ts +75 -986
  76. package/src/route-hydration-contract.ts +22 -0
  77. package/src/route-module-loader.ts +95 -0
  78. package/src/routes.ts +8 -0
  79. package/src/serve.ts +110 -29
  80. package/src/server-action-inference.ts +49 -0
  81. package/src/vite.ts +6 -4
package/src/render.ts CHANGED
@@ -1,14 +1,16 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { access, readFile, stat } from "node:fs/promises";
4
4
  import { dirname, join, relative, sep } from "node:path";
5
5
  import {
6
6
  formatDiagnostic,
7
7
  transform,
8
- type ClientReferenceMetadata,
9
- type ServerOutputMode,
10
- type TransformOutput,
11
8
  } from "@reckona/mreact-compiler";
9
+ import type {
10
+ ClientReferenceMetadata,
11
+ ServerOutputMode,
12
+ TransformOutput,
13
+ } from "@reckona/mreact-shared/compiler-contract";
12
14
  import {
13
15
  createQueryClient,
14
16
  dehydrate,
@@ -26,23 +28,22 @@ import {
26
28
  } from "@reckona/mreact-server";
27
29
  import {
28
30
  createClientRouteInferenceCache,
29
- hydrationMarkerParts,
30
31
  formatClientRouteInferenceDiagnostic,
31
32
  inferClientRouteModule,
32
- routeIdForPath,
33
33
  type ClientRouteInferenceCache,
34
34
  type ClientRouteInferenceResult,
35
+ } from "./client-route-inference.js";
36
+ import {
37
+ hydrationMarkerParts,
38
+ routeIdForPath,
35
39
  withHydrationMarkers,
36
40
  withRouteMarkers,
37
- } from "./client.js";
41
+ } from "./navigation-runtime.js";
38
42
  import { assetPath } from "./assets.js";
39
- import {
40
- escapeHtmlAttribute,
41
- escapeHtmlText as escapeHtml,
42
- } from "@reckona/mreact-shared/html-escape";
43
+ import { escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
43
44
  import { matchRoute, scanAppRoutes } from "./routes.js";
44
45
  import type { AppRoute, RouteMatcher } from "./routes.js";
45
- import { appFileConventionContentType, type AppFileConvention } from "./file-conventions.js";
46
+ import { appFileConventionContentType } from "./file-conventions.js";
46
47
  import {
47
48
  type AppRouterServerActionOptions,
48
49
  type PreparedFormActionReference,
@@ -65,7 +66,6 @@ import {
65
66
  importAppRouterSourceModule,
66
67
  fileImportMetaUrlPlugin,
67
68
  } from "./module-runner.js";
68
- import { contentSecurityPolicy } from "./csp.js";
69
69
  import { bytesResponse, htmlResponse } from "./http.js";
70
70
  import { isNotFoundError, isRedirectError, rewriteLocation } from "./navigation.js";
71
71
  import { createAppRouterImportPolicyPlugin, type AppRouterImportPolicy } from "./import-policy.js";
@@ -90,17 +90,11 @@ import {
90
90
  type RouterRuntimeCacheStat,
91
91
  } from "./cache-stats.js";
92
92
  import { bundleRouterModule } from "./bundle-pipeline.js";
93
- import { routeSecurityHeaders } from "./security-headers.js";
94
93
  import { vitePluginsCacheKey } from "./vite-plugin-cache-key.js";
95
94
  import type {
96
95
  ManifestDescriptor,
97
- MetadataImage,
98
- MetadataScalar,
99
- MetadataThemeColor,
100
- MetadataViewport,
101
96
  RobotsManifest,
102
97
  RouteParams,
103
- RouteHeadDescriptor,
104
98
  RouteMetadata,
105
99
  SitemapEntry,
106
100
  } from "./types.js";
@@ -109,6 +103,41 @@ import {
109
103
  traceContextFromRequest,
110
104
  type RouterInstrumentation,
111
105
  } from "./trace.js";
106
+ import {
107
+ mergeRouteMiddlewareControls,
108
+ middlewareMatches,
109
+ parseRouteMiddlewareControl,
110
+ parseStaticMiddlewareConfig,
111
+ shouldSkipMiddleware,
112
+ type MiddlewareModule,
113
+ type RouteMiddlewareControl,
114
+ } from "./middleware.js";
115
+ import {
116
+ applyFileConventionMetadata,
117
+ injectHeadMetadata,
118
+ mergeRouteMetadata,
119
+ responseHeadersForMetadata,
120
+ serializeRobots,
121
+ serializeSitemap,
122
+ } from "./metadata.js";
123
+ import {
124
+ createSlotRenderContext,
125
+ markShellBoundary,
126
+ shellBoundaryId,
127
+ splitLayoutSlot,
128
+ warnUnconsumedRouteSlots,
129
+ type ShellFile,
130
+ type SlotRenderContext,
131
+ } from "./layout-composer.js";
132
+ import {
133
+ importPolicyCacheKey,
134
+ memoizedHashText,
135
+ prebuiltRequestModuleArtifact,
136
+ prebuiltRouteLoaderModuleArtifact,
137
+ prebuiltServerComponentModuleCode,
138
+ prebuiltServerModuleOutputMatches,
139
+ type BuiltServerModuleOutputLike,
140
+ } from "./route-module-loader.js";
112
141
 
113
142
  const nativeEscapeTransform = {
114
143
  batchImportName: "escapeHtmlBatch",
@@ -422,11 +451,6 @@ type StreamModuleExports = Record<string, unknown> & {
422
451
  slots?: StreamRouteSlotExports;
423
452
  };
424
453
 
425
- interface SlotRenderContext {
426
- consumedSlots: Set<string>;
427
- namedSlots: Readonly<Record<string, string>>;
428
- }
429
-
430
454
  const serverTransformCache = new Map<string, TransformOutput>();
431
455
  const serverTransformCacheCounters = createRouterRuntimeCacheCounters();
432
456
  const serverSourceFileCache = new Map<string, Promise<string>>();
@@ -666,10 +690,6 @@ export type AppRouterMiddlewareResult =
666
690
  | { request: Request; type: "continue" }
667
691
  | { response: Response; type: "response" };
668
692
 
669
- interface RouteMiddlewareControl {
670
- skip?: boolean | readonly string[];
671
- }
672
-
673
693
  export async function resolveAppRouterMiddleware(options: {
674
694
  appDir: string;
675
695
  importPolicy?: AppRouterImportPolicy | undefined;
@@ -1890,6 +1910,33 @@ async function dispatchConventionAssetRoute(options: {
1890
1910
  });
1891
1911
  }
1892
1912
 
1913
+ function textConventionResponse(body: string): Response {
1914
+ return htmlResponse(body, {
1915
+ headers: {
1916
+ "cache-control": "public, max-age=3600",
1917
+ "content-type": "text/plain; charset=utf-8",
1918
+ },
1919
+ });
1920
+ }
1921
+
1922
+ function xmlConventionResponse(body: string): Response {
1923
+ return htmlResponse(body, {
1924
+ headers: {
1925
+ "cache-control": "no-cache",
1926
+ "content-type": "application/xml; charset=utf-8",
1927
+ },
1928
+ });
1929
+ }
1930
+
1931
+ function jsonConventionResponse(body: ManifestDescriptor): Response {
1932
+ return htmlResponse(JSON.stringify(body), {
1933
+ headers: {
1934
+ "cache-control": "public, max-age=3600",
1935
+ "content-type": "application/manifest+json; charset=utf-8",
1936
+ },
1937
+ });
1938
+ }
1939
+
1893
1940
  async function dispatchMetadataRoute(options: {
1894
1941
  file: string;
1895
1942
  params: RouteParams;
@@ -2148,75 +2195,6 @@ async function runMiddleware(options: {
2148
2195
  return undefined;
2149
2196
  }
2150
2197
 
2151
- interface MiddlewareModule {
2152
- config?: {
2153
- id?: string | undefined;
2154
- matcher?: string | RegExp | readonly string[] | undefined;
2155
- };
2156
- default?: unknown;
2157
- middleware?: unknown;
2158
- }
2159
-
2160
- interface StaticMiddlewareConfig {
2161
- hasMatcher: boolean;
2162
- id?: string | undefined;
2163
- matcher?: string | readonly string[] | undefined;
2164
- }
2165
-
2166
- function shouldSkipMiddleware(
2167
- config: Pick<NonNullable<MiddlewareModule["config"]>, "id"> | undefined,
2168
- control: RouteMiddlewareControl | undefined,
2169
- ): boolean {
2170
- if (control?.skip === true) {
2171
- return true;
2172
- }
2173
-
2174
- if (!Array.isArray(control?.skip)) {
2175
- return false;
2176
- }
2177
-
2178
- return typeof config?.id === "string" && control.skip.includes(config.id);
2179
- }
2180
-
2181
- function parseStaticMiddlewareConfig(code: string): StaticMiddlewareConfig {
2182
- const configBody = /\bexport\s+const\s+config\s*=\s*\{(?<body>[\s\S]*?)\}\s*;?/.exec(code)?.groups
2183
- ?.body;
2184
-
2185
- if (configBody === undefined) {
2186
- return { hasMatcher: false };
2187
- }
2188
-
2189
- const id = /\bid\s*:\s*["'](?<id>[^"']+)["']/.exec(configBody)?.groups?.id;
2190
- const stringMatcher = /\bmatcher\s*:\s*["'](?<matcher>[^"']+)["']/.exec(configBody)?.groups
2191
- ?.matcher;
2192
-
2193
- if (stringMatcher !== undefined) {
2194
- return {
2195
- hasMatcher: true,
2196
- ...(id === undefined ? {} : { id }),
2197
- matcher: stringMatcher,
2198
- };
2199
- }
2200
-
2201
- const matcherArray = /\bmatcher\s*:\s*\[(?<items>[\s\S]*?)\]/.exec(configBody)?.groups?.items;
2202
-
2203
- if (matcherArray !== undefined) {
2204
- return {
2205
- hasMatcher: true,
2206
- ...(id === undefined ? {} : { id }),
2207
- matcher: Array.from(
2208
- matcherArray.matchAll(/["'](?<matcher>[^"']+)["']/g),
2209
- (match) => match.groups?.matcher,
2210
- ).filter((matcher): matcher is string => matcher !== undefined),
2211
- };
2212
- }
2213
-
2214
- return {
2215
- hasMatcher: /\bmatcher\s*:/.test(configBody),
2216
- ...(id === undefined ? {} : { id }),
2217
- };
2218
- }
2219
-
2220
2198
  async function loadMiddlewareModule(options: {
2221
2199
  appDir: string;
2222
2200
  code?: string | undefined;
@@ -2348,44 +2326,6 @@ async function loadBundledMiddlewareModule(options: {
2348
2326
  });
2349
2327
  }
2350
2328
 
2351
- function middlewareMatches(config: MiddlewareModule["config"], pathname: string): boolean {
2352
- const matcher = config?.matcher;
2353
-
2354
- if (matcher === undefined) {
2355
- return true;
2356
- }
2357
-
2358
- if (matcher instanceof RegExp) {
2359
- return matcher.test(pathname);
2360
- }
2361
-
2362
- if (Array.isArray(matcher)) {
2363
- return matcher.some((item) => middlewarePatternMatches(item, pathname));
2364
- }
2365
-
2366
- return typeof matcher === "string" && middlewarePatternMatches(matcher, pathname);
2367
- }
2368
-
2369
- function middlewarePatternMatches(pattern: string, pathname: string): boolean {
2370
- if (pattern === pathname) {
2371
- return true;
2372
- }
2373
-
2374
- if (pattern.endsWith("/:path*")) {
2375
- const prefix = pattern.slice(0, -"/:path*".length);
2376
-
2377
- return pathname === prefix || pathname.startsWith(`${prefix}/`);
2378
- }
2379
-
2380
- if (pattern.endsWith("*")) {
2381
- const prefix = pattern.slice(0, -1);
2382
-
2383
- return pathname.startsWith(prefix);
2384
- }
2385
-
2386
- return false;
2387
- }
2388
-
2389
2329
  function transformServerModule(options: {
2390
2330
  code: string;
2391
2331
  clientBoundaryImports?: readonly string[];
@@ -2572,31 +2512,6 @@ function routeSourceFilesForAnalysis(options: {
2572
2512
  : { ...Object.fromEntries(options.serverSourceFiles), [options.filename]: options.code };
2573
2513
  }
2574
2514
 
2575
- // Per-request hashText (SHA-256) is one of the hot path's dominant
2576
- // costs. Cache hashes for `code` strings we have already seen this
2577
- // process (common case: the prepared code is identical across requests
2578
- // when the source file is unchanged).
2579
- const codeHashCache = new Map<string, string>();
2580
- const MAX_CODE_HASH_ENTRIES = 256;
2581
-
2582
- function memoizedHashText(code: string): string {
2583
- const cached = codeHashCache.get(code);
2584
- if (cached !== undefined) {
2585
- return cached;
2586
- }
2587
-
2588
- const hash = hashText(code);
2589
- if (codeHashCache.size >= MAX_CODE_HASH_ENTRIES) {
2590
- // Simple LRU eviction: drop the oldest entry (Map keeps insertion order).
2591
- const oldestKey = codeHashCache.keys().next().value;
2592
- if (oldestKey !== undefined) {
2593
- codeHashCache.delete(oldestKey);
2594
- }
2595
- }
2596
- codeHashCache.set(code, hash);
2597
- return hash;
2598
- }
2599
-
2600
2515
  async function runServerModule(
2601
2516
  code: string,
2602
2517
  props: ServerComponentProps,
@@ -2696,30 +2611,6 @@ async function loadServerModule(
2696
2611
  });
2697
2612
  }
2698
2613
 
2699
- function prebuiltServerComponentModuleCode(
2700
- artifact: BuiltServerModuleArtifact["string"] | BuiltServerModuleArtifact["stream"] | undefined,
2701
- code: string,
2702
- codeHash: string,
2703
- ): string | undefined {
2704
- if (artifact === undefined) {
2705
- return undefined;
2706
- }
2707
-
2708
- if (!prebuiltServerModuleOutputMatches(artifact, code, codeHash)) {
2709
- return undefined;
2710
- }
2711
-
2712
- return artifact.bundleCode;
2713
- }
2714
-
2715
- function prebuiltServerModuleOutputMatches(
2716
- artifact: BuiltServerModuleOutputLike,
2717
- code: string,
2718
- codeHash: string,
2719
- ): boolean {
2720
- return artifact.sourceHash === codeHash || artifact.code === code;
2721
- }
2722
-
2723
2614
  async function importBuiltServerModuleFile<T>(options: {
2724
2615
  file: string;
2725
2616
  label: string;
@@ -3573,29 +3464,6 @@ async function renderShellStreamComponent(
3573
3464
  return sink.toString();
3574
3465
  }
3575
3466
 
3576
- function splitLayoutSlot(
3577
- layoutHtml: string,
3578
- slotContext: SlotRenderContext = createSlotRenderContext(),
3579
- ): Pick<RenderedShell, "prefix" | "suffix"> {
3580
- const html = replaceNamedLayoutSlots(layoutHtml, slotContext);
3581
- const match = findDefaultLayoutSlot(html);
3582
-
3583
- if (match === null) {
3584
- return { prefix: html, suffix: "" };
3585
- }
3586
-
3587
- return {
3588
- prefix: html.slice(0, match.index),
3589
- suffix: html.slice(match.index + match[0].length),
3590
- };
3591
- }
3592
-
3593
- interface ShellFile {
3594
- file: string;
3595
- id: string;
3596
- kind: "layout" | "template";
3597
- }
3598
-
3599
3467
  // Layout/template files for a given page do not change during a server's
3600
3468
  // lifetime in production. Each cache miss costs up to N×4 filesystem
3601
3469
  // `access()` syscalls (~5-10μs each on a fast SSD), making this one of
@@ -3670,14 +3538,6 @@ function withRouteCacheHeader(
3670
3538
  return response;
3671
3539
  }
3672
3540
 
3673
- function shellBoundaryId(appDir: string, directory: string): string {
3674
- const relativeDirectory = relative(appDir, directory);
3675
-
3676
- return relativeDirectory === ""
3677
- ? "root"
3678
- : relativeDirectory.replaceAll(sep, "/").replace(/[^A-Za-z0-9_$/-]/g, "_");
3679
- }
3680
-
3681
3541
  async function hasAppMiddleware(options: {
3682
3542
  appDir: string;
3683
3543
  serverModuleCacheVersion?: string | undefined;
@@ -3844,148 +3704,6 @@ async function loadRouteMiddlewareControlFile(options: {
3844
3704
  return control;
3845
3705
  }
3846
3706
 
3847
- function parseRouteMiddlewareControl(code: string): RouteMiddlewareControl | undefined {
3848
- if (!/\bexport\s+const\s+middleware\s*=/.test(code)) {
3849
- return undefined;
3850
- }
3851
-
3852
- if (/\bmiddleware\s*=\s*\{[\s\S]*?\bskip\s*:\s*true\b/.test(code)) {
3853
- return { skip: true };
3854
- }
3855
-
3856
- const skipArray = /\bmiddleware\s*=\s*\{[\s\S]*?\bskip\s*:\s*\[([\s\S]*?)\]/.exec(code);
3857
-
3858
- if (skipArray === null) {
3859
- return undefined;
3860
- }
3861
-
3862
- const ids = Array.from(
3863
- skipArray[1]?.matchAll(/["']([^"']+)["']/g) ?? [],
3864
- (match) => match[1],
3865
- ).filter((id) => id !== undefined);
3866
-
3867
- return ids.length === 0 ? undefined : { skip: ids };
3868
- }
3869
-
3870
- function mergeRouteMiddlewareControls(
3871
- controls: readonly (RouteMiddlewareControl | undefined)[],
3872
- ): RouteMiddlewareControl | undefined {
3873
- const skippedIds = new Set<string>();
3874
-
3875
- for (const control of controls) {
3876
- if (control?.skip === true) {
3877
- return { skip: true };
3878
- }
3879
-
3880
- if (Array.isArray(control?.skip)) {
3881
- for (const id of control.skip) {
3882
- skippedIds.add(id);
3883
- }
3884
- }
3885
- }
3886
-
3887
- return skippedIds.size === 0 ? undefined : { skip: [...skippedIds] };
3888
- }
3889
-
3890
- function markShellBoundary(html: string, shell: ShellFile): string {
3891
- const attributeName =
3892
- shell.kind === "layout" ? "data-mreact-layout-boundary" : "data-mreact-template-boundary";
3893
-
3894
- if (html.includes(`${attributeName}=`)) {
3895
- return html;
3896
- }
3897
-
3898
- return html.replace(
3899
- /<([A-Za-z][^\s/>]*)([^>]*)>/,
3900
- `<$1$2 ${attributeName}="${escapeHtmlAttribute(shell.id)}">`,
3901
- );
3902
- }
3903
-
3904
- function replaceNamedLayoutSlots(layoutHtml: string, slotContext: SlotRenderContext): string {
3905
- return layoutHtml.replace(SLOT_TAG_PATTERN, (source, openAttributes: string) => {
3906
- const name = readSlotName(openAttributes);
3907
-
3908
- if (name === undefined || name === "default") {
3909
- return source;
3910
- }
3911
-
3912
- if (Object.hasOwn(slotContext.namedSlots, name)) {
3913
- slotContext.consumedSlots.add(name);
3914
- return slotContext.namedSlots[name] ?? "";
3915
- }
3916
-
3917
- return "";
3918
- });
3919
- }
3920
-
3921
- const SLOT_TAG_PATTERN = /<slot\b([^>]*)>(?:<\/slot\s*>)?/g;
3922
-
3923
- function findDefaultLayoutSlot(html: string): RegExpExecArray | null {
3924
- SLOT_TAG_PATTERN.lastIndex = 0;
3925
-
3926
- for (;;) {
3927
- const match = SLOT_TAG_PATTERN.exec(html);
3928
-
3929
- if (match === null) {
3930
- return null;
3931
- }
3932
-
3933
- const name = readSlotName(match[1] ?? "");
3934
-
3935
- if (name === undefined || name === "default") {
3936
- return match;
3937
- }
3938
- }
3939
- }
3940
-
3941
- function readSlotName(attributes: string): string | undefined {
3942
- const match = /\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(attributes);
3943
-
3944
- return match?.[1] ?? match?.[2];
3945
- }
3946
-
3947
- function createSlotRenderContext(
3948
- namedSlots: Readonly<Record<string, string>> = {},
3949
- ): SlotRenderContext {
3950
- return {
3951
- consumedSlots: new Set(),
3952
- namedSlots,
3953
- };
3954
- }
3955
-
3956
- function warnUnconsumedRouteSlots(options: {
3957
- appDir: string;
3958
- pageFile: string;
3959
- serverModuleCacheVersion: string | undefined;
3960
- slotContext: SlotRenderContext;
3961
- }): void {
3962
- if (options.serverModuleCacheVersion !== undefined) {
3963
- return;
3964
- }
3965
-
3966
- const slotNames = Object.keys(options.slotContext.namedSlots);
3967
- if (slotNames.length === 0) {
3968
- return;
3969
- }
3970
-
3971
- const routeLabel = relative(options.appDir, options.pageFile).replaceAll(sep, "/");
3972
-
3973
- for (const name of slotNames) {
3974
- if (name === "default") {
3975
- console.warn(
3976
- `[mreact] ${routeLabel}: slots.default does not target <Slot />; use the page body for default slot content.`,
3977
- );
3978
- continue;
3979
- }
3980
-
3981
- if (!options.slotContext.consumedSlots.has(name)) {
3982
- console.warn(
3983
- `[mreact] ${routeLabel}: slots.{${name}} is not consumed by any ancestor layout or template.`,
3984
- );
3985
- }
3986
- }
3987
- }
3988
-
3989
3707
  interface RouteDataContext {
3990
3708
  params: RouteParams;
3991
3709
  queryClient: QueryClient;
@@ -4011,8 +3729,6 @@ interface RouteMetadataModule {
4011
3729
  metadata?: RouteMetadata;
4012
3730
  }
4013
3731
 
4014
- type CspDirectiveMap = Record<string, readonly string[] | string>;
4015
-
4016
3732
  async function loadRouteData(options: {
4017
3733
  appDir: string;
4018
3734
  code: string;
@@ -4215,45 +3931,6 @@ async function loadBundledRouteLoaderModule(options: {
4215
3931
  });
4216
3932
  }
4217
3933
 
4218
- type BuiltServerModuleOutputLike = NonNullable<BuiltServerModuleArtifact["request"]>;
4219
-
4220
- function prebuiltRequestModuleArtifact(
4221
- serverModules: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined,
4222
- file: string,
4223
- source: string,
4224
- kind: "request" | "routeMetadata" = "request",
4225
- ): BuiltServerModuleOutputLike | undefined {
4226
- const artifact = serverModules?.get(file)?.[kind];
4227
-
4228
- return artifact !== undefined && artifact.sourceHash === memoizedHashText(source)
4229
- ? artifact
4230
- : undefined;
4231
- }
4232
-
4233
- function prebuiltRouteLoaderModuleArtifact(
4234
- serverModules: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined,
4235
- file: string,
4236
- source: string,
4237
- ): BuiltServerModuleOutputLike | undefined {
4238
- const artifact = serverModules?.get(file)?.loader;
4239
-
4240
- return artifact !== undefined && artifact.sourceHash === memoizedHashText(source)
4241
- ? artifact
4242
- : prebuiltRequestModuleArtifact(serverModules, file, source);
4243
- }
4244
-
4245
- function importPolicyCacheKey(policy: AppRouterImportPolicy | undefined): string {
4246
- if (policy === undefined) {
4247
- return "";
4248
- }
4249
-
4250
- return JSON.stringify({
4251
- allowedPackages: [...(policy.allowedPackages ?? [])].sort(),
4252
- allowedSourceDirs: [...(policy.allowedSourceDirs ?? [])].sort(),
4253
- projectRoot: policy.projectRoot ?? "",
4254
- });
4255
- }
4256
-
4257
3934
  async function loadRouteMetadata(options: {
4258
3935
  appDir: string;
4259
3936
  code: string;
@@ -4476,255 +4153,6 @@ async function loadComposedRouteMetadataUncached(options: {
4476
4153
  };
4477
4154
  }
4478
4155
 
4479
- function mergeRouteMetadata(metadata: readonly RouteMetadata[]): RouteMetadata | undefined {
4480
- if (metadata.length === 0) {
4481
- return undefined;
4482
- }
4483
-
4484
- return metadata.reduce<RouteMetadata>((merged, next) => {
4485
- const mergedMetadata: RouteMetadata = { ...merged, ...next };
4486
- const alternates = mergeObject(merged.alternates, next.alternates);
4487
- const csp = mergeCspMetadata(merged.csp, next.csp);
4488
- const head = mergeReadonlyArrays(merged.head, next.head);
4489
- const icons = mergeObject(merged.icons, next.icons);
4490
- const openGraph = mergeOpenGraphMetadata(merged.openGraph, next.openGraph);
4491
-
4492
- if (alternates !== undefined) {
4493
- mergedMetadata.alternates = alternates;
4494
- }
4495
- if (csp !== undefined) {
4496
- mergedMetadata.csp = csp;
4497
- }
4498
- if (head !== undefined) {
4499
- mergedMetadata.head = head;
4500
- }
4501
- if (icons !== undefined) {
4502
- mergedMetadata.icons = icons;
4503
- }
4504
- if (openGraph !== undefined) {
4505
- mergedMetadata.openGraph = openGraph;
4506
- }
4507
-
4508
- return mergedMetadata;
4509
- }, {});
4510
- }
4511
-
4512
- function applyFileConventionMetadata(
4513
- metadata: RouteMetadata | undefined,
4514
- routes: readonly AppRoute[],
4515
- filename: string,
4516
- params: RouteParams,
4517
- ): RouteMetadata | undefined {
4518
- const next: RouteMetadata = metadata === undefined ? {} : { ...metadata };
4519
- const iconRoute = routes.find((route) => route.kind === "asset" && route.convention === "icon");
4520
- const appleIconRoute = routes.find(
4521
- (route) => route.kind === "asset" && route.convention === "apple-icon",
4522
- );
4523
- const openGraphImagePath = fileConventionMetadataRoutePath(
4524
- routes,
4525
- filename,
4526
- params,
4527
- "opengraph-image",
4528
- );
4529
-
4530
- if (iconRoute !== undefined && next.icons?.icon === undefined) {
4531
- next.icons = { ...next.icons, icon: iconRoute.path };
4532
- }
4533
- if (appleIconRoute !== undefined && next.icons?.apple === undefined) {
4534
- next.icons = { ...next.icons, apple: appleIconRoute.path };
4535
- }
4536
- if (
4537
- openGraphImagePath !== undefined &&
4538
- next.openGraph?.image === undefined &&
4539
- (next.openGraph?.images === undefined || next.openGraph.images.length === 0)
4540
- ) {
4541
- next.openGraph = { ...next.openGraph, image: openGraphImagePath };
4542
- }
4543
-
4544
- return Object.keys(next).length === 0 ? undefined : next;
4545
- }
4546
-
4547
- function fileConventionMetadataRoutePath(
4548
- routes: readonly AppRoute[],
4549
- filename: string,
4550
- params: RouteParams,
4551
- convention: AppFileConvention,
4552
- ): string | undefined {
4553
- const pageRoute = routes.find((route) => route.kind === "page" && route.file === filename);
4554
- const candidateRoutes = routes.filter(
4555
- (route) =>
4556
- (route.kind === "asset" || route.kind === "metadata") &&
4557
- route.convention === convention,
4558
- );
4559
-
4560
- if (pageRoute !== undefined) {
4561
- const expectedPath = pageRoute.path === "/" ? `/${convention}` : `${pageRoute.path}/${convention}`;
4562
- const routeLocal = candidateRoutes.find((route) => route.path === expectedPath);
4563
- const routeLocalPath =
4564
- routeLocal === undefined ? undefined : concreteRoutePath(routeLocal.path, params);
4565
-
4566
- if (routeLocalPath !== undefined) {
4567
- return routeLocalPath;
4568
- }
4569
- }
4570
-
4571
- return candidateRoutes.find((route) => route.path === `/${convention}`)?.path;
4572
- }
4573
-
4574
- function concreteRoutePath(path: string, params: RouteParams): string | undefined {
4575
- const segments = path === "/" ? [] : path.slice(1).split("/");
4576
- const concrete: string[] = [];
4577
-
4578
- for (const segment of segments) {
4579
- if (segment.startsWith(":...")) {
4580
- const value = params[segment.slice(4)];
4581
- const values = Array.isArray(value)
4582
- ? value
4583
- : typeof value === "string"
4584
- ? value.split("/").filter((part) => part !== "")
4585
- : undefined;
4586
-
4587
- if (values === undefined) {
4588
- return undefined;
4589
- }
4590
- concrete.push(...values.map((part) => encodeURIComponent(part)));
4591
- continue;
4592
- }
4593
-
4594
- if (segment.startsWith(":")) {
4595
- const value = params[segment.slice(1)];
4596
- const stringValue = Array.isArray(value) ? value[0] : value;
4597
-
4598
- if (typeof stringValue !== "string") {
4599
- return undefined;
4600
- }
4601
- concrete.push(encodeURIComponent(stringValue));
4602
- continue;
4603
- }
4604
-
4605
- concrete.push(segment);
4606
- }
4607
-
4608
- return `/${concrete.join("/")}`;
4609
- }
4610
-
4611
- function mergeObject<T extends object>(left: T | undefined, right: T | undefined): T | undefined {
4612
- if (left === undefined) {
4613
- return right;
4614
- }
4615
-
4616
- if (right === undefined) {
4617
- return left;
4618
- }
4619
-
4620
- return { ...left, ...right };
4621
- }
4622
-
4623
- function mergeReadonlyArrays<T>(
4624
- left: readonly T[] | undefined,
4625
- right: readonly T[] | undefined,
4626
- ): readonly T[] | undefined {
4627
- if (left === undefined || left.length === 0) {
4628
- return right;
4629
- }
4630
-
4631
- if (right === undefined || right.length === 0) {
4632
- return left;
4633
- }
4634
-
4635
- return [...left, ...right];
4636
- }
4637
-
4638
- function mergeCspMetadata(
4639
- left: RouteMetadata["csp"],
4640
- right: RouteMetadata["csp"],
4641
- ): RouteMetadata["csp"] | undefined {
4642
- if (right?.disable === true) {
4643
- return { disable: true };
4644
- }
4645
-
4646
- if (left === undefined) {
4647
- if (right === undefined) {
4648
- return undefined;
4649
- }
4650
-
4651
- const merged: NonNullable<RouteMetadata["csp"]> = { ...right };
4652
- const directives = applyCspOverrides(undefined, right);
4653
-
4654
- if (directives !== undefined) {
4655
- merged.directives = directives;
4656
- } else {
4657
- delete merged.directives;
4658
- }
4659
-
4660
- return merged;
4661
- }
4662
-
4663
- if (right === undefined) {
4664
- return left;
4665
- }
4666
-
4667
- const merged: NonNullable<RouteMetadata["csp"]> = {
4668
- ...left,
4669
- ...right,
4670
- };
4671
- const directives = applyCspOverrides(left.directives, right);
4672
-
4673
- if (directives !== undefined) {
4674
- merged.directives = directives;
4675
- } else {
4676
- delete merged.directives;
4677
- }
4678
-
4679
- return merged;
4680
- }
4681
-
4682
- function applyCspOverrides(
4683
- left: CspDirectiveMap | undefined,
4684
- right: RouteMetadata["csp"] | undefined,
4685
- ): CspDirectiveMap | undefined {
4686
- if (right === undefined) {
4687
- return left;
4688
- }
4689
-
4690
- const merged = { ...left, ...right.directives };
4691
-
4692
- for (const [name, value] of Object.entries(right.replace ?? {})) {
4693
- merged[name] = value;
4694
- }
4695
-
4696
- for (const name of right.remove ?? []) {
4697
- delete merged[name];
4698
- }
4699
-
4700
- return Object.keys(merged).length === 0 ? undefined : merged;
4701
- }
4702
-
4703
- function mergeOpenGraphMetadata(
4704
- left: RouteMetadata["openGraph"],
4705
- right: RouteMetadata["openGraph"],
4706
- ): RouteMetadata["openGraph"] | undefined {
4707
- if (left === undefined) {
4708
- return right;
4709
- }
4710
-
4711
- if (right === undefined) {
4712
- return left;
4713
- }
4714
-
4715
- const merged: NonNullable<RouteMetadata["openGraph"]> = {
4716
- ...left,
4717
- ...right,
4718
- };
4719
- const images = mergeReadonlyArrays(openGraphImages(left), openGraphImages(right));
4720
-
4721
- if (images !== undefined && images.length > 0) {
4722
- merged.images = images;
4723
- }
4724
-
4725
- return merged;
4726
- }
4727
-
4728
4156
  function hasMetadataExport(code: string): boolean {
4729
4157
  return /\bexport\s+const\s+metadata\s*=/.test(code) || hasGenerateMetadataExport(code);
4730
4158
  }
@@ -4740,213 +4168,6 @@ function usesRuntimeCacheControl(code: string): boolean {
4740
4168
  return /\bcacheControl\s*\(/.test(code);
4741
4169
  }
4742
4170
 
4743
- function injectHeadMetadata(html: string, metadata: RouteMetadata | undefined): string {
4744
- if (metadata === undefined) {
4745
- return html;
4746
- }
4747
-
4748
- let nextHtml =
4749
- metadata.lang === undefined
4750
- ? html
4751
- : injectHtmlLangAttribute(html, metadataString(metadata.lang, "lang"));
4752
- const tags = [
4753
- metadata.title === undefined
4754
- ? undefined
4755
- : `<title>${escapeHtml(metadataString(metadata.title, "title"))}</title>`,
4756
- metadata.description === undefined
4757
- ? undefined
4758
- : `<meta name="description" content="${escapeHtmlAttribute(metadataString(metadata.description, "description"))}">`,
4759
- metadata.alternates?.canonical === undefined
4760
- ? undefined
4761
- : `<link rel="canonical" href="${escapeHtmlAttribute(metadataString(metadata.alternates.canonical, "alternates.canonical"))}">`,
4762
- metadata.openGraph?.title === undefined
4763
- ? undefined
4764
- : `<meta property="og:title" content="${escapeHtmlAttribute(metadataString(metadata.openGraph.title, "openGraph.title"))}">`,
4765
- metadata.openGraph?.description === undefined
4766
- ? undefined
4767
- : `<meta property="og:description" content="${escapeHtmlAttribute(metadataString(metadata.openGraph.description, "openGraph.description"))}">`,
4768
- ...openGraphImages(metadata.openGraph).map(
4769
- (image) => `<meta property="og:image" content="${escapeHtmlAttribute(image)}">`,
4770
- ),
4771
- metadata.icons?.icon === undefined
4772
- ? undefined
4773
- : `<link rel="icon" href="${escapeHtmlAttribute(metadataString(metadata.icons.icon, "icons.icon"))}">`,
4774
- metadata.icons?.apple === undefined
4775
- ? undefined
4776
- : `<link rel="apple-touch-icon" href="${escapeHtmlAttribute(metadataString(metadata.icons.apple, "icons.apple"))}">`,
4777
- metadata.robots === undefined
4778
- ? undefined
4779
- : `<meta name="robots" content="${escapeHtmlAttribute(robotsContent(metadata.robots))}">`,
4780
- metadata.themeColor === undefined ? undefined : themeColorTag(metadata.themeColor),
4781
- metadata.viewport === undefined
4782
- ? undefined
4783
- : `<meta name="viewport" content="${escapeHtmlAttribute(viewportContent(metadata.viewport))}">`,
4784
- ...headDescriptorTags(metadata.head, metadata.csp?.nonce),
4785
- ]
4786
- .filter((tag): tag is string => tag !== undefined)
4787
- .join("");
4788
-
4789
- if (tags === "") {
4790
- return nextHtml;
4791
- }
4792
-
4793
- if (/<head(?:\s[^>]*)?>/i.test(nextHtml)) {
4794
- return nextHtml.replace(/<head(\s[^>]*)?>/i, (match) => `${match}${tags}`);
4795
- }
4796
-
4797
- if (/<html(?:\s[^>]*)?>/i.test(nextHtml)) {
4798
- return nextHtml.replace(/<html(\s[^>]*)?>/i, (match) => `${match}<head>${tags}</head>`);
4799
- }
4800
-
4801
- return `<head>${tags}</head>${nextHtml}`;
4802
- }
4803
-
4804
- function injectHtmlLangAttribute(html: string, lang: string): string {
4805
- const escapedLang = escapeHtmlAttribute(lang);
4806
-
4807
- if (!/<html(?:\s[^>]*)?>/i.test(html)) {
4808
- return html;
4809
- }
4810
-
4811
- return html.replace(/<html(\s[^>]*)?>/i, (_match, attrs = "") => {
4812
- const strippedAttrs = String(attrs).replace(/\s+lang=(?:"[^"]*"|'[^']*'|[^\s>]+)/i, "");
4813
- return `<html lang="${escapedLang}"${strippedAttrs}>`;
4814
- });
4815
- }
4816
-
4817
- const DEFAULT_HTML_RESPONSE_HEADERS = Object.freeze({
4818
- "content-type": "text/html; charset=utf-8",
4819
- });
4820
-
4821
- function responseHeadersForMetadata(
4822
- metadata: RouteMetadata | undefined,
4823
- request: Request,
4824
- extra?: Readonly<Record<string, string>>,
4825
- ): HeadersInit {
4826
- const csp = contentSecurityPolicy(metadata?.csp);
4827
- const security = routeSecurityHeaders({
4828
- request,
4829
- security: metadata?.security,
4830
- });
4831
-
4832
- if (csp === undefined && extra === undefined) {
4833
- return {
4834
- ...DEFAULT_HTML_RESPONSE_HEADERS,
4835
- ...security,
4836
- };
4837
- }
4838
-
4839
- return {
4840
- ...DEFAULT_HTML_RESPONSE_HEADERS,
4841
- ...security,
4842
- ...(csp === undefined ? undefined : { "content-security-policy": csp }),
4843
- ...extra,
4844
- };
4845
- }
4846
-
4847
- function textConventionResponse(body: string): Response {
4848
- return htmlResponse(body, {
4849
- headers: {
4850
- "cache-control": "public, max-age=3600",
4851
- "content-type": "text/plain; charset=utf-8",
4852
- },
4853
- });
4854
- }
4855
-
4856
- function xmlConventionResponse(body: string): Response {
4857
- return htmlResponse(body, {
4858
- headers: {
4859
- "cache-control": "no-cache",
4860
- "content-type": "application/xml; charset=utf-8",
4861
- },
4862
- });
4863
- }
4864
-
4865
- function jsonConventionResponse(body: ManifestDescriptor): Response {
4866
- return htmlResponse(JSON.stringify(body), {
4867
- headers: {
4868
- "cache-control": "public, max-age=3600",
4869
- "content-type": "application/manifest+json; charset=utf-8",
4870
- },
4871
- });
4872
- }
4873
-
4874
- function serializeRobots(manifest: RobotsManifest): string {
4875
- const lines: string[] = [];
4876
- const rules =
4877
- manifest.rules === undefined
4878
- ? []
4879
- : Array.isArray(manifest.rules)
4880
- ? manifest.rules
4881
- : [manifest.rules];
4882
-
4883
- for (const rule of rules) {
4884
- for (const userAgent of arrayValue(rule.userAgent)) {
4885
- lines.push(`User-agent: ${userAgent}`);
4886
- }
4887
- for (const allow of arrayValue(rule.allow)) {
4888
- lines.push(`Allow: ${allow}`);
4889
- }
4890
- for (const disallow of arrayValue(rule.disallow)) {
4891
- lines.push(`Disallow: ${disallow}`);
4892
- }
4893
- }
4894
-
4895
- for (const sitemap of arrayValue(manifest.sitemap)) {
4896
- lines.push(`Sitemap: ${sitemap}`);
4897
- }
4898
- if (manifest.host !== undefined) {
4899
- lines.push(`Host: ${manifest.host}`);
4900
- }
4901
-
4902
- return `${lines.join("\n")}\n`;
4903
- }
4904
-
4905
- function serializeSitemap(entries: readonly SitemapEntry[]): string {
4906
- const urls = entries
4907
- .map((entry) => {
4908
- const fields = [
4909
- `<loc>${escapeXml(entry.url)}</loc>`,
4910
- entry.lastModified === undefined
4911
- ? undefined
4912
- : `<lastmod>${escapeXml(sitemapDate(entry.lastModified))}</lastmod>`,
4913
- entry.changeFrequency === undefined
4914
- ? undefined
4915
- : `<changefreq>${escapeXml(entry.changeFrequency)}</changefreq>`,
4916
- entry.priority === undefined ? undefined : `<priority>${entry.priority}</priority>`,
4917
- ].filter((field): field is string => field !== undefined);
4918
-
4919
- return `<url>${fields.join("")}</url>`;
4920
- })
4921
- .join("");
4922
-
4923
- return `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}</urlset>`;
4924
- }
4925
-
4926
- function sitemapDate(value: Date | number | string): string {
4927
- if (value instanceof Date) {
4928
- return value.toISOString();
4929
- }
4930
-
4931
- return typeof value === "number" ? new Date(value).toISOString() : value;
4932
- }
4933
-
4934
- function arrayValue<T>(value: T | readonly T[] | undefined): readonly T[] {
4935
- if (value === undefined) {
4936
- return [];
4937
- }
4938
-
4939
- return Array.isArray(value) ? (value as readonly T[]) : [value as T];
4940
- }
4941
-
4942
- function escapeXml(value: string): string {
4943
- return value
4944
- .replaceAll("&", "&amp;")
4945
- .replaceAll("<", "&lt;")
4946
- .replaceAll(">", "&gt;")
4947
- .replaceAll('"', "&quot;");
4948
- }
4949
-
4950
4171
  function injectQueryState(html: string, state: DehydratedQueryClient): string {
4951
4172
  if (state.queries.length === 0) {
4952
4173
  return html;
@@ -5001,134 +4222,6 @@ function escapeJsonForHtml(value: string): string {
5001
4222
  .replaceAll("\u2029", "\\u2029");
5002
4223
  }
5003
4224
 
5004
- function headDescriptorTags(
5005
- descriptors: readonly RouteHeadDescriptor[] | undefined,
5006
- nonce: string | undefined,
5007
- ): string[] {
5008
- return (descriptors ?? []).flatMap((descriptor) => {
5009
- const descriptorNonce = descriptor.nonce === true ? nonce : descriptor.nonce || undefined;
5010
- const attrs: Record<string, boolean | number | string | undefined> = {
5011
- ...descriptor.attrs,
5012
- ...(descriptorNonce === undefined ? {} : { nonce: descriptorNonce }),
5013
- };
5014
- const attrText = Object.entries(attrs)
5015
- .flatMap(([name, value]) => {
5016
- if (value === undefined || value === false) {
5017
- return [];
5018
- }
5019
-
5020
- return value === true
5021
- ? [escapeHtmlAttribute(name)]
5022
- : [`${escapeHtmlAttribute(name)}="${escapeHtmlAttribute(String(value))}"`];
5023
- })
5024
- .join(" ");
5025
- const open = attrText === "" ? `<${descriptor.tag}>` : `<${descriptor.tag} ${attrText}>`;
5026
-
5027
- if (descriptor.tag === "meta" || descriptor.tag === "link" || descriptor.tag === "base") {
5028
- return [open.slice(0, -1) + ">"];
5029
- }
5030
-
5031
- return [`${open}${escapeHeadTextContent(descriptor.content ?? "")}</${descriptor.tag}>`];
5032
- });
5033
- }
5034
-
5035
- function escapeHeadTextContent(value: string): string {
5036
- return value.replaceAll("<", "\\u003c");
5037
- }
5038
-
5039
- function metadataString(value: MetadataScalar, path: string): string {
5040
- if (isMetadataScalar(value)) {
5041
- return String(value);
5042
- }
5043
-
5044
- throw new Error(`Invalid metadata field ${path}: expected string, number, or boolean.`);
5045
- }
5046
-
5047
- function metadataKebabName(name: string): string {
5048
- return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
5049
- }
5050
-
5051
- function viewportContent(viewport: MetadataScalar | MetadataViewport): string {
5052
- if (isMetadataScalar(viewport)) {
5053
- return metadataString(viewport, "viewport");
5054
- }
5055
-
5056
- return Object.entries(viewport)
5057
- .flatMap(([key, value]) => {
5058
- if (value === undefined || value === null || value === false) {
5059
- return [];
5060
- }
5061
-
5062
- return [`${metadataKebabName(key)}=${metadataString(value, `viewport.${key}`)}`];
5063
- })
5064
- .join(", ");
5065
- }
5066
-
5067
- function themeColorTag(themeColor: MetadataScalar | MetadataThemeColor): string {
5068
- if (isMetadataScalar(themeColor)) {
5069
- return `<meta name="theme-color" content="${escapeHtmlAttribute(metadataString(themeColor, "themeColor"))}">`;
5070
- }
5071
-
5072
- const content = themeColor.color;
5073
- if (!isMetadataScalar(content)) {
5074
- throw new Error(
5075
- "Invalid metadata field themeColor.color: expected string, number, or boolean.",
5076
- );
5077
- }
5078
-
5079
- const media =
5080
- themeColor.media === undefined
5081
- ? ""
5082
- : ` media="${escapeHtmlAttribute(metadataString(metadataScalarField(themeColor.media, "themeColor.media"), "themeColor.media"))}"`;
5083
-
5084
- return `<meta name="theme-color"${media} content="${escapeHtmlAttribute(metadataString(content, "themeColor.color"))}">`;
5085
- }
5086
-
5087
- function metadataScalarField(value: unknown, path: string): MetadataScalar {
5088
- if (isMetadataScalar(value)) {
5089
- return value;
5090
- }
5091
-
5092
- throw new Error(`Invalid metadata field ${path}: expected string, number, or boolean.`);
5093
- }
5094
-
5095
- function isMetadataScalar(value: unknown): value is MetadataScalar {
5096
- return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
5097
- }
5098
-
5099
- function openGraphImages(openGraph: RouteMetadata["openGraph"]): readonly string[] {
5100
- if (openGraph?.images !== undefined) {
5101
- return openGraph.images.map((image, index) =>
5102
- metadataImageUrl(image, `openGraph.images.${index}`),
5103
- );
5104
- }
5105
-
5106
- return openGraph?.image === undefined ? [] : [metadataImageUrl(openGraph.image, "openGraph.image")];
5107
- }
5108
-
5109
- function metadataImageUrl(value: MetadataImage | MetadataScalar, path: string): string {
5110
- if (isMetadataScalar(value)) {
5111
- return metadataString(value, path);
5112
- }
5113
-
5114
- if (typeof value === "object" && value !== null && "url" in value) {
5115
- return metadataString(value.url, `${path}.url`);
5116
- }
5117
-
5118
- throw new Error(`Invalid metadata field ${path}: expected string, number, boolean, or object with url.`);
5119
- }
5120
-
5121
- function robotsContent(robots: NonNullable<RouteMetadata["robots"]>): string {
5122
- if (typeof robots === "string") {
5123
- return robots;
5124
- }
5125
-
5126
- return [
5127
- robots.index === false ? "noindex" : "index",
5128
- robots.follow === false ? "nofollow" : "follow",
5129
- ].join(",");
5130
- }
5131
-
5132
4225
  function readServerSourceFile(
5133
4226
  file: string,
5134
4227
  serverModuleCacheVersion: string | undefined,
@@ -5170,10 +4263,6 @@ function readServerSourceFile(
5170
4263
  return loaded;
5171
4264
  }
5172
4265
 
5173
- function hashText(text: string): string {
5174
- return createHash("sha256").update(text).digest("hex").slice(0, 16);
5175
- }
5176
-
5177
4266
  function setBoundedCacheEntry<K, V>(
5178
4267
  cache: Map<K, V>,
5179
4268
  key: K,