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