@reckona/mreact-router 0.0.152 → 0.0.154
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 +1 -1
- package/dist/actions.d.ts +10 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +6 -0
- package/dist/actions.js.map +1 -1
- package/dist/adapters/aws-lambda.d.ts +18 -0
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +18 -0
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +33 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +33 -0
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/edge.d.ts +5 -0
- package/dist/adapters/edge.d.ts.map +1 -1
- package/dist/adapters/edge.js +5 -0
- package/dist/adapters/edge.js.map +1 -1
- package/dist/adapters/node.d.ts +5 -0
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +5 -0
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts +5 -0
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +5 -0
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +85 -2
- package/dist/build.js.map +1 -1
- package/dist/cache.d.ts +15 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +15 -0
- package/dist/cache.js.map +1 -1
- package/dist/cookies.d.ts +16 -0
- package/dist/cookies.d.ts.map +1 -1
- package/dist/cookies.js +16 -0
- package/dist/cookies.js.map +1 -1
- package/dist/dev-server.d.ts +5 -0
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +5 -0
- package/dist/dev-server.js.map +1 -1
- package/dist/link.d.ts +10 -0
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +5 -0
- package/dist/link.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +39 -4
- package/dist/middleware.js.map +1 -1
- package/dist/module-runner.d.ts +6 -0
- package/dist/module-runner.d.ts.map +1 -1
- package/dist/module-runner.js +71 -0
- package/dist/module-runner.js.map +1 -1
- package/dist/navigation.d.ts +53 -0
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +48 -0
- package/dist/navigation.js.map +1 -1
- package/dist/render.d.ts +34 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +161 -27
- package/dist/render.js.map +1 -1
- package/dist/route-module-loader.d.ts +6 -1
- package/dist/route-module-loader.d.ts.map +1 -1
- package/dist/route-module-loader.js +8 -1
- package/dist/route-module-loader.js.map +1 -1
- package/dist/serve.d.ts +5 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +5 -0
- package/dist/serve.js.map +1 -1
- package/dist/session.d.ts +19 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +19 -0
- package/dist/session.js.map +1 -1
- package/dist/typed-routes.d.ts +5 -0
- package/dist/typed-routes.d.ts.map +1 -1
- package/dist/typed-routes.js +5 -0
- package/dist/typed-routes.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +1 -0
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +18 -0
- package/src/adapters/aws-lambda.ts +18 -0
- package/src/adapters/cloudflare.ts +33 -0
- package/src/adapters/edge.ts +5 -0
- package/src/adapters/node.ts +5 -0
- package/src/adapters/static.ts +5 -0
- package/src/build.ts +118 -1
- package/src/cache.ts +15 -0
- package/src/cookies.ts +16 -0
- package/src/dev-server.ts +5 -0
- package/src/link.ts +10 -0
- package/src/middleware.ts +54 -4
- package/src/module-runner.ts +91 -0
- package/src/navigation.ts +53 -0
- package/src/render.ts +229 -25
- package/src/route-module-loader.ts +15 -0
- package/src/serve.ts +5 -0
- package/src/session.ts +19 -0
- package/src/typed-routes.ts +5 -0
- package/src/types.ts +15 -0
- package/src/vite.ts +1 -0
package/src/render.ts
CHANGED
|
@@ -137,6 +137,7 @@ import {
|
|
|
137
137
|
prebuiltRouteLoaderModuleArtifact,
|
|
138
138
|
prebuiltServerComponentModuleCode,
|
|
139
139
|
prebuiltServerModuleOutputMatches,
|
|
140
|
+
prebuiltServerModuleOutputOptionsMatch,
|
|
140
141
|
type BuiltServerModuleOutputLike,
|
|
141
142
|
} from "./route-module-loader.js";
|
|
142
143
|
|
|
@@ -184,6 +185,9 @@ export interface RenderAppRequestOptions {
|
|
|
184
185
|
routes?: readonly AppRoute[] | undefined;
|
|
185
186
|
serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
|
|
186
187
|
serverModuleCacheVersion?: string | undefined;
|
|
188
|
+
// True when serving through a dev server: request-time server transforms
|
|
189
|
+
// are expected there and must not trigger the production prebuild warning.
|
|
190
|
+
dev?: boolean | undefined;
|
|
187
191
|
serverSourceFiles?: ReadonlyMap<string, string> | undefined;
|
|
188
192
|
serverActions?: AppRouterServerActionOptions | undefined;
|
|
189
193
|
serverActionReferencesByFile?:
|
|
@@ -353,6 +357,8 @@ async function preloadBuiltPageRouteModules(options: {
|
|
|
353
357
|
options.serverModuleCacheVersion,
|
|
354
358
|
options.define,
|
|
355
359
|
options.vitePlugins,
|
|
360
|
+
undefined,
|
|
361
|
+
{ serverAwaitHydration: options.analysis.clientInference.client },
|
|
356
362
|
);
|
|
357
363
|
}
|
|
358
364
|
|
|
@@ -476,6 +482,7 @@ type StreamModuleExports = Record<string, unknown> & {
|
|
|
476
482
|
|
|
477
483
|
const serverTransformCache = new Map<string, TransformOutput>();
|
|
478
484
|
const serverTransformCacheCounters = createRouterRuntimeCacheCounters();
|
|
485
|
+
const productionDynamicServerTransformWarned = new Set<string>();
|
|
479
486
|
const serverSourceFileCache = new Map<string, Promise<string>>();
|
|
480
487
|
const serverSourceFileCacheCounters = createRouterRuntimeCacheCounters();
|
|
481
488
|
const routeSourceAnalysisCache = new Map<string, Promise<RouteSourceAnalysis>>();
|
|
@@ -640,6 +647,17 @@ function finishRenderTimingPhase(
|
|
|
640
647
|
timing.phases[phaseName] = logDurationMs(startedAt);
|
|
641
648
|
}
|
|
642
649
|
|
|
650
|
+
async function readSettledPromiseAtNextTask<T>(
|
|
651
|
+
promise: Promise<T>,
|
|
652
|
+
): Promise<{ settled: true; value: T } | { settled: false }> {
|
|
653
|
+
return await Promise.race([
|
|
654
|
+
promise.then((value) => ({ settled: true as const, value })),
|
|
655
|
+
new Promise<{ settled: false }>((resolve) => {
|
|
656
|
+
setTimeout(() => resolve({ settled: false }), 0);
|
|
657
|
+
}),
|
|
658
|
+
]);
|
|
659
|
+
}
|
|
660
|
+
|
|
643
661
|
function addRenderTimingPhaseDuration(
|
|
644
662
|
timing: RenderTiming | undefined,
|
|
645
663
|
startedAt: number | undefined,
|
|
@@ -652,6 +670,12 @@ function addRenderTimingPhaseDuration(
|
|
|
652
670
|
timing.phases[phaseName] = (timing.phases[phaseName] ?? 0) + logDurationMs(startedAt);
|
|
653
671
|
}
|
|
654
672
|
|
|
673
|
+
function routeLoaderMayReturnControlResponse(code: string): boolean {
|
|
674
|
+
return /\b(?:redirect|redirectExternal|rewrite|notFound|throwNotFound)\s*\(/.test(code) ||
|
|
675
|
+
/\bnew\s+Response\s*\(/.test(code) ||
|
|
676
|
+
/\bResponse\.(?:redirect|json)\s*\(/.test(code);
|
|
677
|
+
}
|
|
678
|
+
|
|
655
679
|
async function waitForRenderPreload(
|
|
656
680
|
options: Pick<RenderAppRequestOptions, "preload">,
|
|
657
681
|
timing: RenderTiming | undefined,
|
|
@@ -745,6 +769,7 @@ export async function resolveAppRouterMiddleware(options: {
|
|
|
745
769
|
}
|
|
746
770
|
|
|
747
771
|
async function renderAppRequestInternal(options: RenderAppRequestOptions): Promise<Response> {
|
|
772
|
+
warnProductionRenderWithoutPrebuiltModules(options);
|
|
748
773
|
const timing = createRenderTiming(options.logger);
|
|
749
774
|
const clientRouteInferenceCache =
|
|
750
775
|
options.clientRouteInferenceCache ?? createClientRouteInferenceCache();
|
|
@@ -835,6 +860,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
835
860
|
appDir: options.appDir,
|
|
836
861
|
filename: "not-found.mreact.tsx",
|
|
837
862
|
pathname: url.pathname,
|
|
863
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
838
864
|
});
|
|
839
865
|
|
|
840
866
|
const response = await renderSpecialRoute({
|
|
@@ -1208,8 +1234,9 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
1208
1234
|
}
|
|
1209
1235
|
|
|
1210
1236
|
let streamData: unknown;
|
|
1211
|
-
|
|
1212
|
-
|
|
1237
|
+
let streamDataPromise = dataPromise ?? Promise.resolve(undefined);
|
|
1238
|
+
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
1239
|
+
if (loadingFile === undefined || routeLoaderMayReturnControlResponse(code)) {
|
|
1213
1240
|
try {
|
|
1214
1241
|
streamData = dataPromise === undefined ? undefined : await dataPromise;
|
|
1215
1242
|
} finally {
|
|
@@ -1219,6 +1246,20 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
1219
1246
|
emitRenderTiming(options, timing, streamData.status);
|
|
1220
1247
|
return streamData;
|
|
1221
1248
|
}
|
|
1249
|
+
streamDataPromise = Promise.resolve(streamData);
|
|
1250
|
+
} else {
|
|
1251
|
+
try {
|
|
1252
|
+
const settledData = await readSettledPromiseAtNextTask(streamDataPromise);
|
|
1253
|
+
if (settledData.settled) {
|
|
1254
|
+
if (settledData.value instanceof Response) {
|
|
1255
|
+
emitRenderTiming(options, timing, settledData.value.status);
|
|
1256
|
+
return settledData.value;
|
|
1257
|
+
}
|
|
1258
|
+
streamDataPromise = Promise.resolve(settledData.value);
|
|
1259
|
+
}
|
|
1260
|
+
} finally {
|
|
1261
|
+
finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
|
|
1262
|
+
}
|
|
1222
1263
|
}
|
|
1223
1264
|
|
|
1224
1265
|
await waitForRenderPreload(options, timing);
|
|
@@ -1249,7 +1290,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
1249
1290
|
appDir: options.appDir,
|
|
1250
1291
|
assetBaseUrl: options.assetBaseUrl,
|
|
1251
1292
|
clientRoute,
|
|
1252
|
-
data:
|
|
1293
|
+
data: streamDataPromise,
|
|
1253
1294
|
define: options.define,
|
|
1254
1295
|
loadingFile,
|
|
1255
1296
|
pageFile: matched.route.file,
|
|
@@ -1737,16 +1778,18 @@ async function nearestBoundaryFileForPath(options: {
|
|
|
1737
1778
|
appDir: string;
|
|
1738
1779
|
filename: string;
|
|
1739
1780
|
pathname: string;
|
|
1781
|
+
serverSourceFiles?: ReadonlyMap<string, string> | undefined;
|
|
1740
1782
|
}): Promise<string> {
|
|
1741
1783
|
const parts = options.pathname
|
|
1742
1784
|
.replace(/^\/+|\/+$/g, "")
|
|
1743
1785
|
.split("/")
|
|
1744
|
-
.filter((part) => part.length > 0);
|
|
1786
|
+
.filter((part) => part.length > 0 && part !== "." && part !== "..");
|
|
1745
1787
|
|
|
1746
|
-
return nearestBoundaryFileFromParts({
|
|
1788
|
+
return await nearestBoundaryFileFromParts({
|
|
1747
1789
|
appDir: options.appDir,
|
|
1748
1790
|
filename: options.filename,
|
|
1749
1791
|
parts,
|
|
1792
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
1750
1793
|
});
|
|
1751
1794
|
}
|
|
1752
1795
|
|
|
@@ -1754,11 +1797,19 @@ async function nearestBoundaryFileFromParts(options: {
|
|
|
1754
1797
|
appDir: string;
|
|
1755
1798
|
filename: string;
|
|
1756
1799
|
parts: string[];
|
|
1800
|
+
serverSourceFiles?: ReadonlyMap<string, string> | undefined;
|
|
1757
1801
|
}): Promise<string> {
|
|
1758
1802
|
for (let count = options.parts.length; count >= 0; count -= 1) {
|
|
1759
1803
|
for (const filename of boundaryFilenameCandidates(options.filename)) {
|
|
1760
1804
|
const candidate = join(options.appDir, ...options.parts.slice(0, count), filename);
|
|
1761
1805
|
|
|
1806
|
+
if (options.serverSourceFiles !== undefined) {
|
|
1807
|
+
if (options.serverSourceFiles.has(candidate)) {
|
|
1808
|
+
return candidate;
|
|
1809
|
+
}
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1762
1813
|
try {
|
|
1763
1814
|
await access(candidate);
|
|
1764
1815
|
return candidate;
|
|
@@ -1835,10 +1886,16 @@ async function renderSpecialRoute(options: {
|
|
|
1835
1886
|
textFallback: string;
|
|
1836
1887
|
vitePlugins?: readonly PluginOption[] | undefined;
|
|
1837
1888
|
}): Promise<Response> {
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1889
|
+
if (options.serverSourceFiles !== undefined) {
|
|
1890
|
+
if (!options.serverSourceFiles.has(options.routeFile)) {
|
|
1891
|
+
return new Response(options.textFallback, { status: options.status });
|
|
1892
|
+
}
|
|
1893
|
+
} else {
|
|
1894
|
+
try {
|
|
1895
|
+
await access(options.routeFile);
|
|
1896
|
+
} catch {
|
|
1897
|
+
return new Response(options.textFallback, { status: options.status });
|
|
1898
|
+
}
|
|
1842
1899
|
}
|
|
1843
1900
|
|
|
1844
1901
|
const props = {
|
|
@@ -1982,7 +2039,10 @@ async function dispatchServerRoute(options: {
|
|
|
1982
2039
|
const handler = module[options.request.method] ?? module.ALL ?? module.default;
|
|
1983
2040
|
|
|
1984
2041
|
if (typeof handler !== "function") {
|
|
1985
|
-
return new Response("Method Not Allowed", {
|
|
2042
|
+
return new Response("Method Not Allowed", {
|
|
2043
|
+
headers: { allow: allowedServerRouteMethods(module).join(", ") },
|
|
2044
|
+
status: 405,
|
|
2045
|
+
});
|
|
1986
2046
|
}
|
|
1987
2047
|
|
|
1988
2048
|
let response: unknown;
|
|
@@ -2007,6 +2067,12 @@ async function dispatchServerRoute(options: {
|
|
|
2007
2067
|
: new Response("Invalid route response", { status: 500 });
|
|
2008
2068
|
}
|
|
2009
2069
|
|
|
2070
|
+
function allowedServerRouteMethods(module: Record<string, unknown>): string[] {
|
|
2071
|
+
return ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].filter(
|
|
2072
|
+
(method) => typeof module[method] === "function",
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2010
2076
|
async function dispatchConventionAssetRoute(options: {
|
|
2011
2077
|
file: string;
|
|
2012
2078
|
request: Request;
|
|
@@ -2544,7 +2610,10 @@ function transformServerModule(options: {
|
|
|
2544
2610
|
if (
|
|
2545
2611
|
artifact !== undefined &&
|
|
2546
2612
|
artifact.sourceHash === sourceHash &&
|
|
2547
|
-
|
|
2613
|
+
prebuiltServerModuleOutputOptionsMatch(
|
|
2614
|
+
artifact,
|
|
2615
|
+
options.serverAwaitHydration === true ? { serverAwaitHydration: true } : {},
|
|
2616
|
+
)
|
|
2548
2617
|
) {
|
|
2549
2618
|
return {
|
|
2550
2619
|
code: artifact.code,
|
|
@@ -2578,6 +2647,23 @@ function transformServerModule(options: {
|
|
|
2578
2647
|
return cached;
|
|
2579
2648
|
}
|
|
2580
2649
|
|
|
2650
|
+
// Only renders that were handed a prebuild map can have a prebuild gap;
|
|
2651
|
+
// dev servers pass no serverModules and transform at request time by design.
|
|
2652
|
+
if (options.serverModules !== undefined) {
|
|
2653
|
+
warnProductionDynamicServerTransform(
|
|
2654
|
+
options.filename,
|
|
2655
|
+
options.serverOutput,
|
|
2656
|
+
artifact === undefined
|
|
2657
|
+
? "no prebuilt server module for this output mode"
|
|
2658
|
+
: !prebuiltServerModuleOutputOptionsMatch(
|
|
2659
|
+
artifact,
|
|
2660
|
+
options.serverAwaitHydration === true ? { serverAwaitHydration: true } : {},
|
|
2661
|
+
)
|
|
2662
|
+
? `serverAwaitHydration mismatch (artifact=${artifact.metadata?.serverAwaitHydration === true}, requested=${options.serverAwaitHydration === true})`
|
|
2663
|
+
: "stale prebuilt server module (source hash changed)",
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2581
2667
|
const output = transform({
|
|
2582
2668
|
code: options.code,
|
|
2583
2669
|
...(options.clientBoundaryImports === undefined
|
|
@@ -2605,6 +2691,65 @@ function transformServerModule(options: {
|
|
|
2605
2691
|
return output;
|
|
2606
2692
|
}
|
|
2607
2693
|
|
|
2694
|
+
export const __transformServerModuleForTesting = transformServerModule;
|
|
2695
|
+
|
|
2696
|
+
let productionRenderWithoutPrebuiltModulesWarned = false;
|
|
2697
|
+
|
|
2698
|
+
export function __resetProductionRenderWarningsForTesting(): void {
|
|
2699
|
+
productionRenderWithoutPrebuiltModulesWarned = false;
|
|
2700
|
+
productionDynamicServerTransformWarned.clear();
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// Covers the deployment that forgot prebuilt modules entirely; per-module
|
|
2704
|
+
// gaps inside a provided prebuild map are reported by
|
|
2705
|
+
// warnProductionDynamicServerTransform with the affected file instead.
|
|
2706
|
+
export function __warnProductionRenderWithoutPrebuiltModulesForTesting(options: {
|
|
2707
|
+
dev?: boolean | undefined;
|
|
2708
|
+
serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
|
|
2709
|
+
}): void {
|
|
2710
|
+
warnProductionRenderWithoutPrebuiltModules(options);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
function warnProductionRenderWithoutPrebuiltModules(options: {
|
|
2714
|
+
dev?: boolean | undefined;
|
|
2715
|
+
serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
|
|
2716
|
+
}): void {
|
|
2717
|
+
if (
|
|
2718
|
+
process.env.NODE_ENV !== "production" ||
|
|
2719
|
+
options.dev === true ||
|
|
2720
|
+
options.serverModules !== undefined ||
|
|
2721
|
+
productionRenderWithoutPrebuiltModulesWarned
|
|
2722
|
+
) {
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
productionRenderWithoutPrebuiltModulesWarned = true;
|
|
2727
|
+
console.warn(
|
|
2728
|
+
"mreact router: production render without prebuilt serverModules. Request-time transforms use development compiler settings; pass the build output's serverModules (or set dev: true for dev servers).",
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
function warnProductionDynamicServerTransform(
|
|
2733
|
+
filename: string,
|
|
2734
|
+
serverOutput: ServerOutputMode,
|
|
2735
|
+
reason: string,
|
|
2736
|
+
): void {
|
|
2737
|
+
if (process.env.NODE_ENV !== "production") {
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
const warnedKey = `${filename}\0${serverOutput}`;
|
|
2742
|
+
|
|
2743
|
+
if (productionDynamicServerTransformWarned.has(warnedKey)) {
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
productionDynamicServerTransformWarned.add(warnedKey);
|
|
2748
|
+
console.warn(
|
|
2749
|
+
`mreact router: dynamic server transform path ran in production for ${filename} (serverOutput=${serverOutput}): ${reason}. Production deployments should provide prebuilt serverModules; otherwise request-time transforms use development compiler settings.`,
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2608
2753
|
async function analyzeRouteSource(options: {
|
|
2609
2754
|
appDir: string;
|
|
2610
2755
|
artifact?: BuiltRouteSourceAnalysisSummary | undefined;
|
|
@@ -3442,12 +3587,14 @@ async function loadServerStreamModule(
|
|
|
3442
3587
|
define?: UserConfig["define"] | undefined,
|
|
3443
3588
|
vitePlugins?: readonly PluginOption[] | undefined,
|
|
3444
3589
|
importPolicy?: AppRouterImportPolicy | undefined,
|
|
3590
|
+
outputOptions: { serverAwaitHydration?: boolean } = {},
|
|
3445
3591
|
): Promise<StreamModuleExports> {
|
|
3446
3592
|
const artifactCode = serverModules?.get(sourcefile)?.stream;
|
|
3447
3593
|
const codeHash = memoizedHashText(code);
|
|
3448
|
-
const prebuiltCode = prebuiltServerComponentModuleCode(artifactCode, code, codeHash);
|
|
3594
|
+
const prebuiltCode = prebuiltServerComponentModuleCode(artifactCode, code, codeHash, outputOptions);
|
|
3449
3595
|
if (
|
|
3450
3596
|
artifactCode !== undefined &&
|
|
3597
|
+
prebuiltServerModuleOutputOptionsMatch(artifactCode, outputOptions) &&
|
|
3451
3598
|
prebuiltServerModuleOutputMatches(artifactCode, code, codeHash) &&
|
|
3452
3599
|
artifactCode.moduleFile !== undefined
|
|
3453
3600
|
) {
|
|
@@ -4539,10 +4686,7 @@ async function loadComposedRouteMetadata(options: {
|
|
|
4539
4686
|
serverSourceFiles?: ReadonlyMap<string, string> | undefined;
|
|
4540
4687
|
vitePlugins?: readonly PluginOption[] | undefined;
|
|
4541
4688
|
}): Promise<RouteMetadata | undefined> {
|
|
4542
|
-
const cacheKey =
|
|
4543
|
-
options.serverModuleCacheVersion === undefined
|
|
4544
|
-
? undefined
|
|
4545
|
-
: `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
|
|
4689
|
+
const cacheKey = composedRouteMetadataCacheKey(options);
|
|
4546
4690
|
if (cacheKey !== undefined) {
|
|
4547
4691
|
const cached = readRouterRuntimeCacheEntry(
|
|
4548
4692
|
composedRouteMetadataCache,
|
|
@@ -4575,6 +4719,22 @@ async function loadComposedRouteMetadata(options: {
|
|
|
4575
4719
|
}
|
|
4576
4720
|
}
|
|
4577
4721
|
|
|
4722
|
+
function composedRouteMetadataCacheKey(options: {
|
|
4723
|
+
appDir: string;
|
|
4724
|
+
code: string;
|
|
4725
|
+
define?: UserConfig["define"] | undefined;
|
|
4726
|
+
filename: string;
|
|
4727
|
+
importPolicy?: AppRouterImportPolicy | undefined;
|
|
4728
|
+
serverModuleCacheVersion?: string | undefined;
|
|
4729
|
+
vitePlugins?: readonly PluginOption[] | undefined;
|
|
4730
|
+
}): string | undefined {
|
|
4731
|
+
return options.serverModuleCacheVersion === undefined
|
|
4732
|
+
? undefined
|
|
4733
|
+
: `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
export const __composedRouteMetadataCacheKeyForTesting = composedRouteMetadataCacheKey;
|
|
4737
|
+
|
|
4578
4738
|
async function loadComposedRouteMetadataUncached(options: {
|
|
4579
4739
|
appDir: string;
|
|
4580
4740
|
code: string;
|
|
@@ -4775,26 +4935,59 @@ interface InlineCspTag {
|
|
|
4775
4935
|
|
|
4776
4936
|
function inlineCspTags(html: string): InlineCspTag[] {
|
|
4777
4937
|
const tags: InlineCspTag[] = [];
|
|
4778
|
-
const
|
|
4779
|
-
let
|
|
4938
|
+
const lowerHtml = html.toLowerCase();
|
|
4939
|
+
let position = 0;
|
|
4940
|
+
|
|
4941
|
+
while (position < html.length) {
|
|
4942
|
+
const scriptIndex = lowerHtml.indexOf("<script", position);
|
|
4943
|
+
const styleIndex = lowerHtml.indexOf("<style", position);
|
|
4944
|
+
const openerIndex =
|
|
4945
|
+
scriptIndex === -1
|
|
4946
|
+
? styleIndex
|
|
4947
|
+
: styleIndex === -1
|
|
4948
|
+
? scriptIndex
|
|
4949
|
+
: Math.min(scriptIndex, styleIndex);
|
|
4950
|
+
|
|
4951
|
+
if (openerIndex === -1) {
|
|
4952
|
+
break;
|
|
4953
|
+
}
|
|
4780
4954
|
|
|
4781
|
-
|
|
4782
|
-
const
|
|
4955
|
+
const name = lowerHtml.startsWith("<script", openerIndex) ? "script" : "style";
|
|
4956
|
+
const tagEnd = html.indexOf(">", openerIndex + 1);
|
|
4957
|
+
if (tagEnd === -1) {
|
|
4958
|
+
break;
|
|
4959
|
+
}
|
|
4783
4960
|
|
|
4784
|
-
|
|
4961
|
+
const closeToken = `</${name}>`;
|
|
4962
|
+
const closeIndex = lowerHtml.indexOf(closeToken, tagEnd + 1);
|
|
4963
|
+
const nextScriptIndex = lowerHtml.indexOf("<script", tagEnd + 1);
|
|
4964
|
+
const nextStyleIndex = lowerHtml.indexOf("<style", tagEnd + 1);
|
|
4965
|
+
const nextOpenerIndex =
|
|
4966
|
+
nextScriptIndex === -1
|
|
4967
|
+
? nextStyleIndex
|
|
4968
|
+
: nextStyleIndex === -1
|
|
4969
|
+
? nextScriptIndex
|
|
4970
|
+
: Math.min(nextScriptIndex, nextStyleIndex);
|
|
4971
|
+
|
|
4972
|
+
if (closeIndex === -1 || (nextOpenerIndex !== -1 && nextOpenerIndex < closeIndex)) {
|
|
4973
|
+
position = tagEnd + 1;
|
|
4785
4974
|
continue;
|
|
4786
4975
|
}
|
|
4787
4976
|
|
|
4788
4977
|
tags.push({
|
|
4789
|
-
attributes: parseTagAttributes(
|
|
4790
|
-
content:
|
|
4978
|
+
attributes: parseTagAttributes(html.slice(openerIndex + name.length + 1, tagEnd)),
|
|
4979
|
+
content: html.slice(tagEnd + 1, closeIndex),
|
|
4791
4980
|
name,
|
|
4792
4981
|
});
|
|
4982
|
+
|
|
4983
|
+
position = closeIndex + closeToken.length;
|
|
4793
4984
|
}
|
|
4794
4985
|
|
|
4795
4986
|
return tags;
|
|
4796
4987
|
}
|
|
4797
4988
|
|
|
4989
|
+
export const __inlineCspTagsForTesting = inlineCspTags;
|
|
4990
|
+
|
|
4798
4991
|
function parseTagAttributes(source: string): ReadonlyMap<string, string> {
|
|
4799
4992
|
const attributes = new Map<string, string>();
|
|
4800
4993
|
const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
@@ -4842,7 +5035,7 @@ function injectQueryState(html: string, state: DehydratedQueryClient): string {
|
|
|
4842
5035
|
)}</script>`;
|
|
4843
5036
|
|
|
4844
5037
|
return /<\/body>/i.test(html)
|
|
4845
|
-
? html
|
|
5038
|
+
? replaceFinalBodyCloseTag(html, script)
|
|
4846
5039
|
: `${html}${script}`;
|
|
4847
5040
|
}
|
|
4848
5041
|
|
|
@@ -4856,10 +5049,21 @@ function injectAuthSessionClaims(html: string, claims: unknown): string {
|
|
|
4856
5049
|
)}</script>`;
|
|
4857
5050
|
|
|
4858
5051
|
return /<\/body>/i.test(html)
|
|
4859
|
-
? html
|
|
5052
|
+
? replaceFinalBodyCloseTag(html, script)
|
|
4860
5053
|
: `${html}${script}`;
|
|
4861
5054
|
}
|
|
4862
5055
|
|
|
5056
|
+
function replaceFinalBodyCloseTag(html: string, insertion: string): string {
|
|
5057
|
+
const matches = [...html.matchAll(/<\/body>/gi)];
|
|
5058
|
+
const last = matches.at(-1);
|
|
5059
|
+
|
|
5060
|
+
if (last?.index === undefined) {
|
|
5061
|
+
return `${html}${insertion}`;
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
return `${html.slice(0, last.index)}${insertion}${html.slice(last.index)}`;
|
|
5065
|
+
}
|
|
5066
|
+
|
|
4863
5067
|
function authIncludesClaims(code: string): boolean {
|
|
4864
5068
|
return /\bexport\s+const\s+auth\s*=\s*["']include-claims["']\s*;?/.test(code);
|
|
4865
5069
|
}
|
|
@@ -58,11 +58,16 @@ export function prebuiltServerComponentModuleCode(
|
|
|
58
58
|
artifact: BuiltServerModuleArtifact["string"] | BuiltServerModuleArtifact["stream"] | undefined,
|
|
59
59
|
code: string,
|
|
60
60
|
codeHash: string,
|
|
61
|
+
options: { serverAwaitHydration?: boolean } = {},
|
|
61
62
|
): string | undefined {
|
|
62
63
|
if (artifact === undefined) {
|
|
63
64
|
return undefined;
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
if (!prebuiltServerModuleOutputOptionsMatch(artifact, options)) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
66
71
|
if (!prebuiltServerModuleOutputMatches(artifact, code, codeHash)) {
|
|
67
72
|
return undefined;
|
|
68
73
|
}
|
|
@@ -70,6 +75,16 @@ export function prebuiltServerComponentModuleCode(
|
|
|
70
75
|
return artifact.bundleCode;
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
export function prebuiltServerModuleOutputOptionsMatch(
|
|
79
|
+
artifact: NonNullable<BuiltServerModuleArtifact["string"] | BuiltServerModuleArtifact["stream"]>,
|
|
80
|
+
options: { serverAwaitHydration?: boolean },
|
|
81
|
+
): boolean {
|
|
82
|
+
return (
|
|
83
|
+
(artifact.metadata?.serverAwaitHydration === true) ===
|
|
84
|
+
(options.serverAwaitHydration === true)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
export function prebuiltServerModuleOutputMatches(
|
|
74
89
|
artifact: BuiltServerModuleOutputLike,
|
|
75
90
|
code: string,
|
package/src/serve.ts
CHANGED
|
@@ -577,6 +577,11 @@ async function materializeResponseAsBuffer(response: Response): Promise<Response
|
|
|
577
577
|
});
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
+
/**
|
|
581
|
+
* Starts a Node HTTP server for an already built app-router output directory.
|
|
582
|
+
*
|
|
583
|
+
* Use this for production-like local serving or custom Node deployments. It loads `.mreact/server` artifacts, applies the generated import policy when configured, enforces host trust options, and returns a `close()` method for orderly shutdown.
|
|
584
|
+
*/
|
|
580
585
|
export async function startServer(
|
|
581
586
|
options: StartServerOptions,
|
|
582
587
|
): Promise<{ close(): Promise<void>; server: Server; url: string }> {
|
package/src/session.ts
CHANGED
|
@@ -72,6 +72,11 @@ function createSessionId(): string {
|
|
|
72
72
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Creates a process-local session store backed by an in-memory LRU-like map.
|
|
77
|
+
*
|
|
78
|
+
* Use it for development, tests, or single-process deployments; production multi-instance deployments should provide a shared `SessionStore`.
|
|
79
|
+
*/
|
|
75
80
|
export function createMemorySessionStore<TData>(
|
|
76
81
|
options: MemorySessionStoreOptions = {},
|
|
77
82
|
): SessionStore<TData> {
|
|
@@ -152,6 +157,9 @@ function nonNegativeIntegerOrDefault(value: number | undefined, fallback: number
|
|
|
152
157
|
return value === undefined || !Number.isInteger(value) || value < 0 ? fallback : value;
|
|
153
158
|
}
|
|
154
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Reads the current session cookie, loads the matching session record, and deletes expired records.
|
|
162
|
+
*/
|
|
155
163
|
export async function getSession<TData>(
|
|
156
164
|
request: Request,
|
|
157
165
|
store: SessionStore<TData>,
|
|
@@ -177,6 +185,9 @@ export async function getSession<TData>(
|
|
|
177
185
|
return record;
|
|
178
186
|
}
|
|
179
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Creates a new session record, stores it, and appends the session cookie to the response.
|
|
190
|
+
*/
|
|
180
191
|
export async function createSession<TData>(
|
|
181
192
|
response: Response,
|
|
182
193
|
store: SessionStore<TData>,
|
|
@@ -200,6 +211,9 @@ export async function createSession<TData>(
|
|
|
200
211
|
return record;
|
|
201
212
|
}
|
|
202
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Deletes the current session record when present and appends an expiring session cookie to the response.
|
|
216
|
+
*/
|
|
203
217
|
export async function destroySession<TData>(
|
|
204
218
|
request: Request,
|
|
205
219
|
response: Response,
|
|
@@ -219,6 +233,11 @@ export async function destroySession<TData>(
|
|
|
219
233
|
});
|
|
220
234
|
}
|
|
221
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Replaces the current session id while preserving the stored session data.
|
|
238
|
+
*
|
|
239
|
+
* Use this after authentication or privilege changes to reduce session fixation risk.
|
|
240
|
+
*/
|
|
222
241
|
export async function rotateSession<TData>(
|
|
223
242
|
request: Request,
|
|
224
243
|
response: Response,
|
package/src/typed-routes.ts
CHANGED
|
@@ -54,6 +54,11 @@ export type AppRouteLinkSegment<Segment extends string> = Segment extends `:${st
|
|
|
54
54
|
? string
|
|
55
55
|
: Segment;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Builds an internal app-route URL from a route pattern and typed params/search options.
|
|
59
|
+
*
|
|
60
|
+
* Dynamic `:param` and `:...catchAll` segments are URL-encoded, search values are serialized with `URLSearchParams`, and protocol-relative or external paths are rejected.
|
|
61
|
+
*/
|
|
57
62
|
export function href<const Path extends `/${string}`>(
|
|
58
63
|
path: Path,
|
|
59
64
|
...args: HasRouteParams<Path> extends true
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface LoaderContext<TParams extends RouteParams = RouteParams> {
|
|
|
25
25
|
request: Request;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Context passed to a route `generateMetadata` export.
|
|
30
|
+
*
|
|
31
|
+
* Metadata is evaluated on the server after loader data is available, so it can derive head tags, robots data, sitemap entries, and security header directives from the same request and route params as the page.
|
|
32
|
+
*/
|
|
28
33
|
export interface GenerateMetadataContext<
|
|
29
34
|
TData = unknown,
|
|
30
35
|
TParams extends RouteParams = RouteParams,
|
|
@@ -49,6 +54,11 @@ export type PageComponent<TLoader extends RouteLoader> = (
|
|
|
49
54
|
props: PageProps<InferLoaderData<TLoader>, InferLoaderParams<TLoader>>,
|
|
50
55
|
) => ReactCompatNode;
|
|
51
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Preserves the relationship between a page component and its loader when authoring route files.
|
|
59
|
+
*
|
|
60
|
+
* Wrap a default page export with `definePage<typeof loader>(...)` to infer `data` and `params` props from the route loader without changing runtime behavior.
|
|
61
|
+
*/
|
|
52
62
|
export function definePage<TLoader extends RouteLoader>(
|
|
53
63
|
component: PageComponent<TLoader>,
|
|
54
64
|
): PageComponent<TLoader> {
|
|
@@ -73,6 +83,11 @@ export interface MetadataImage {
|
|
|
73
83
|
width?: MetadataScalar;
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Declarative metadata returned by route metadata exports.
|
|
88
|
+
*
|
|
89
|
+
* Values are rendered into the document head and response headers by the app router; use `generateMetadata` when the values depend on loader data, params, or the current request.
|
|
90
|
+
*/
|
|
76
91
|
export interface RouteMetadata {
|
|
77
92
|
alternates?: {
|
|
78
93
|
canonical?: MetadataScalar;
|
package/src/vite.ts
CHANGED