@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.
- package/README.md +2 -0
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +17 -22
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +3 -1
- package/dist/build.js.map +1 -1
- package/dist/client-route-inference.d.ts +2 -0
- package/dist/client-route-inference.d.ts.map +1 -0
- package/dist/client-route-inference.js +2 -0
- package/dist/client-route-inference.js.map +1 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +89 -24
- package/dist/client.js.map +1 -1
- package/dist/layout-composer.d.ts +24 -0
- package/dist/layout-composer.d.ts.map +1 -0
- package/dist/layout-composer.js +83 -0
- package/dist/layout-composer.js.map +1 -0
- package/dist/metadata.d.ts +9 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +419 -0
- package/dist/metadata.js.map +1 -0
- package/dist/middleware.d.ts +22 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +94 -0
- package/dist/middleware.js.map +1 -0
- package/dist/module-runner.d.ts +2 -2
- package/dist/module-runner.d.ts.map +1 -1
- package/dist/module-runner.js +1 -1
- package/dist/module-runner.js.map +1 -1
- package/dist/navigation-runtime.d.ts +2 -0
- package/dist/navigation-runtime.d.ts.map +1 -0
- package/dist/navigation-runtime.js +2 -0
- package/dist/navigation-runtime.js.map +1 -0
- package/dist/preload-policy.d.ts +24 -0
- package/dist/preload-policy.d.ts.map +1 -0
- package/dist/preload-policy.js +45 -0
- package/dist/preload-policy.js.map +1 -0
- package/dist/render.d.ts +1 -3
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +32 -676
- package/dist/render.js.map +1 -1
- package/dist/route-hydration-contract.d.ts +11 -0
- package/dist/route-hydration-contract.d.ts.map +1 -0
- package/dist/route-hydration-contract.js +21 -0
- package/dist/route-hydration-contract.js.map +1 -0
- package/dist/route-module-loader.d.ts +10 -0
- package/dist/route-module-loader.d.ts.map +1 -0
- package/dist/route-module-loader.js +61 -0
- package/dist/route-module-loader.js.map +1 -0
- package/dist/routes.js +6 -0
- package/dist/routes.js.map +1 -1
- package/dist/serve.d.ts +13 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +63 -28
- package/dist/serve.js.map +1 -1
- package/dist/server-action-inference.d.ts.map +1 -1
- package/dist/server-action-inference.js +37 -1
- package/dist/server-action-inference.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +2 -1
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/adapters/aws-lambda.ts +21 -28
- package/src/build.ts +9 -6
- package/src/client-route-inference.ts +18 -0
- package/src/client.ts +124 -24
- package/src/layout-composer.ts +142 -0
- package/src/metadata.ts +578 -0
- package/src/middleware.ts +153 -0
- package/src/module-runner.ts +3 -2
- package/src/navigation-runtime.ts +16 -0
- package/src/preload-policy.ts +89 -0
- package/src/render.ts +75 -986
- package/src/route-hydration-contract.ts +22 -0
- package/src/route-module-loader.ts +95 -0
- package/src/routes.ts +8 -0
- package/src/serve.ts +110 -29
- package/src/server-action-inference.ts +49 -0
- package/src/vite.ts +6 -4
package/src/render.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
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 "./
|
|
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
|
|
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("&", "&")
|
|
4945
|
-
.replaceAll("<", "<")
|
|
4946
|
-
.replaceAll(">", ">")
|
|
4947
|
-
.replaceAll('"', """);
|
|
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,
|