@reckona/mreact-router 0.0.138 → 0.0.139
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 +1 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +32 -12
- package/dist/actions.js.map +1 -1
- package/dist/adapters/static.js +10 -4
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +28 -21
- package/dist/build.js.map +1 -1
- package/dist/cache.d.ts +2 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +23 -1
- package/dist/cache.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +49 -12
- package/dist/client.js.map +1 -1
- package/dist/dev-server.d.ts +1 -0
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +6 -4
- package/dist/dev-server.js.map +1 -1
- package/dist/link.js +17 -3
- package/dist/link.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +10 -7
- package/dist/middleware.js.map +1 -1
- package/dist/multipart.d.ts +5 -0
- package/dist/multipart.d.ts.map +1 -1
- package/dist/multipart.js +24 -1
- package/dist/multipart.js.map +1 -1
- package/dist/native-route-matcher.d.ts +9 -0
- package/dist/native-route-matcher.d.ts.map +1 -1
- package/dist/native-route-matcher.js +23 -11
- package/dist/native-route-matcher.js.map +1 -1
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +8 -1
- package/dist/navigation.js.map +1 -1
- package/dist/render.d.ts +5 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +239 -95
- package/dist/render.js.map +1 -1
- package/dist/route-source.d.ts +3 -1
- package/dist/route-source.d.ts.map +1 -1
- package/dist/route-source.js +13 -11
- package/dist/route-source.js.map +1 -1
- package/dist/route-styles.d.ts +4 -0
- package/dist/route-styles.d.ts.map +1 -1
- package/dist/route-styles.js +15 -3
- package/dist/route-styles.js.map +1 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +39 -9
- package/dist/serve.js.map +1 -1
- package/dist/server-action-inference.d.ts.map +1 -1
- package/dist/server-action-inference.js +57 -0
- package/dist/server-action-inference.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +27 -11
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +44 -10
- package/src/adapters/static.ts +11 -4
- package/src/build.ts +28 -17
- package/src/cache.ts +33 -1
- package/src/client.ts +49 -12
- package/src/dev-server.ts +11 -4
- package/src/link.ts +24 -3
- package/src/middleware.ts +12 -7
- package/src/multipart.ts +32 -1
- package/src/native-route-matcher.ts +34 -13
- package/src/navigation.ts +11 -2
- package/src/render.ts +321 -113
- package/src/route-source.ts +14 -10
- package/src/route-styles.ts +28 -3
- package/src/serve.ts +68 -15
- package/src/server-action-inference.ts +86 -0
- package/src/vite.ts +39 -12
package/src/render.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
createQueryClient,
|
|
13
13
|
dehydrate,
|
|
14
14
|
__MREACT_QUERY_STATE_SCRIPT_ID,
|
|
15
|
+
installQueryAsyncStorage,
|
|
15
16
|
runWithQueryClient,
|
|
16
17
|
type DehydratedQueryClient,
|
|
17
18
|
type QueryClient,
|
|
@@ -40,7 +41,7 @@ import {
|
|
|
40
41
|
import { assetPath } from "./assets.js";
|
|
41
42
|
import { escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
|
|
42
43
|
import { matchRoute, scanAppRoutes } from "./routes.js";
|
|
43
|
-
import type { AppRoute, RouteMatcher } from "./routes.js";
|
|
44
|
+
import type { AppRoute, MatchedRoute, RouteMatcher } from "./routes.js";
|
|
44
45
|
import { appFileConventionContentType } from "./file-conventions.js";
|
|
45
46
|
import {
|
|
46
47
|
type AppRouterServerActionOptions,
|
|
@@ -144,6 +145,8 @@ const nativeEscapeTransform = {
|
|
|
144
145
|
const authRuntimeStateKey = "__mreactAuthRuntimeState";
|
|
145
146
|
const authSessionScriptId = "__mreact_auth_session";
|
|
146
147
|
|
|
148
|
+
installQueryAsyncStorage(new AsyncLocalStorage<QueryClient>());
|
|
149
|
+
|
|
147
150
|
interface AuthRuntimeRequestState {
|
|
148
151
|
claims?: unknown;
|
|
149
152
|
}
|
|
@@ -159,6 +162,7 @@ interface RenderTiming {
|
|
|
159
162
|
export interface RenderAppRequestOptions {
|
|
160
163
|
appDir: string;
|
|
161
164
|
assetBaseUrl?: string | undefined;
|
|
165
|
+
clientRouteInferenceCache?: ClientRouteInferenceCache | undefined;
|
|
162
166
|
clientScripts?: ReadonlyMap<string, string>;
|
|
163
167
|
clientStylesByFile?: ReadonlyMap<string, readonly string[]>;
|
|
164
168
|
clientStyles?: ReadonlyMap<string, readonly string[]>;
|
|
@@ -171,6 +175,8 @@ export interface RenderAppRequestOptions {
|
|
|
171
175
|
onResponse?: AppRouterResponseHook | undefined;
|
|
172
176
|
queryClient?: QueryClient | undefined;
|
|
173
177
|
request: Request;
|
|
178
|
+
requestUrl?: URL | undefined;
|
|
179
|
+
matchedRoute?: MatchedRoute | undefined;
|
|
174
180
|
routeCache?: AppRouterCache | undefined;
|
|
175
181
|
routeMatcher?: RouteMatcher | undefined;
|
|
176
182
|
routes?: readonly AppRoute[] | undefined;
|
|
@@ -595,7 +601,7 @@ export async function renderAppRequest(options: RenderAppRequestOptions): Promis
|
|
|
595
601
|
}
|
|
596
602
|
|
|
597
603
|
const trace = traceContextFromRequest(options.request);
|
|
598
|
-
const url = new URL(options.request.url);
|
|
604
|
+
const url = options.requestUrl ?? new URL(options.request.url);
|
|
599
605
|
const requestEvent = {
|
|
600
606
|
method: options.request.method,
|
|
601
607
|
path: url.pathname,
|
|
@@ -603,7 +609,7 @@ export async function renderAppRequest(options: RenderAppRequestOptions): Promis
|
|
|
603
609
|
...(trace === undefined ? {} : { trace }),
|
|
604
610
|
};
|
|
605
611
|
invokeRouterInstrumentation(options.instrumentation?.onRequestStart, requestEvent);
|
|
606
|
-
const response = await renderAppRequestInternal(options);
|
|
612
|
+
const response = await renderAppRequestInternal({ ...options, requestUrl: url });
|
|
607
613
|
invokeRouterInstrumentation(options.instrumentation?.onRequestEnd, {
|
|
608
614
|
...requestEvent,
|
|
609
615
|
status: response.status,
|
|
@@ -738,13 +744,15 @@ export async function resolveAppRouterMiddleware(options: {
|
|
|
738
744
|
|
|
739
745
|
async function renderAppRequestInternal(options: RenderAppRequestOptions): Promise<Response> {
|
|
740
746
|
const timing = createRenderTiming(options.logger);
|
|
741
|
-
const clientRouteInferenceCache =
|
|
747
|
+
const clientRouteInferenceCache =
|
|
748
|
+
options.clientRouteInferenceCache ?? createClientRouteInferenceCache();
|
|
742
749
|
let phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
743
750
|
const routes = options.routes ?? (await scanAppRoutes({ appDir: options.appDir }));
|
|
744
751
|
finishRenderTimingPhase(timing, phaseStartedAt, "routeScanMs");
|
|
745
|
-
const url = new URL(options.request.url);
|
|
752
|
+
const url = options.requestUrl ?? new URL(options.request.url);
|
|
746
753
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
747
|
-
const matched =
|
|
754
|
+
const matched =
|
|
755
|
+
options.matchedRoute ?? options.routeMatcher?.match(url.pathname) ?? matchRoute(routes, url.pathname);
|
|
748
756
|
finishRenderTimingPhase(timing, phaseStartedAt, "routeMatchMs");
|
|
749
757
|
const hasMiddleware =
|
|
750
758
|
options.skipMiddleware === true
|
|
@@ -790,7 +798,9 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
790
798
|
if (middlewareResult.request !== options.request) {
|
|
791
799
|
return renderAppRequestInternal({
|
|
792
800
|
...options,
|
|
801
|
+
matchedRoute: undefined,
|
|
793
802
|
request: middlewareResult.request,
|
|
803
|
+
requestUrl: new URL(middlewareResult.request.url),
|
|
794
804
|
skipMiddleware: true,
|
|
795
805
|
});
|
|
796
806
|
}
|
|
@@ -943,6 +953,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
943
953
|
: await cachedRouteResponse({
|
|
944
954
|
cache: options.routeCache,
|
|
945
955
|
key: cacheKey,
|
|
956
|
+
request: options.request,
|
|
946
957
|
});
|
|
947
958
|
finishRenderTimingPhase(timing, phaseStartedAt, "routeCacheMs");
|
|
948
959
|
|
|
@@ -1146,6 +1157,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
1146
1157
|
serverModules: options.serverModules,
|
|
1147
1158
|
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
1148
1159
|
serverSourceFiles: options.serverSourceFiles,
|
|
1160
|
+
define: options.define,
|
|
1149
1161
|
vitePlugins: options.vitePlugins,
|
|
1150
1162
|
});
|
|
1151
1163
|
html = injectHeadMetadata(html, metadata);
|
|
@@ -1454,6 +1466,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
1454
1466
|
cache: options.routeCache,
|
|
1455
1467
|
path: matched.route.path,
|
|
1456
1468
|
policy: effectiveCachePolicy,
|
|
1469
|
+
request: options.request,
|
|
1457
1470
|
response,
|
|
1458
1471
|
});
|
|
1459
1472
|
finishRenderTimingPhase(timing, phaseStartedAt, "responseBuildMs");
|
|
@@ -1518,7 +1531,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
1518
1531
|
vitePlugins: options.vitePlugins,
|
|
1519
1532
|
navigation: recoveryRoute,
|
|
1520
1533
|
status: 500,
|
|
1521
|
-
textFallback:
|
|
1534
|
+
textFallback: "Internal Server Error",
|
|
1522
1535
|
});
|
|
1523
1536
|
emitRenderTiming(options, timing, response.status);
|
|
1524
1537
|
return response;
|
|
@@ -2633,10 +2646,9 @@ function routeSourceFilesForAnalysis(options: {
|
|
|
2633
2646
|
code: string;
|
|
2634
2647
|
filename: string;
|
|
2635
2648
|
serverSourceFiles?: ReadonlyMap<string, string> | undefined;
|
|
2636
|
-
}):
|
|
2637
|
-
return
|
|
2638
|
-
|
|
2639
|
-
: { ...Object.fromEntries(options.serverSourceFiles), [options.filename]: options.code };
|
|
2649
|
+
}): (file: string) => string | undefined {
|
|
2650
|
+
return (file) =>
|
|
2651
|
+
file === options.filename ? options.code : options.serverSourceFiles?.get(file);
|
|
2640
2652
|
}
|
|
2641
2653
|
|
|
2642
2654
|
async function runServerModule(
|
|
@@ -3445,9 +3457,8 @@ async function applyLayouts(options: {
|
|
|
3445
3457
|
let html = options.html;
|
|
3446
3458
|
let shellHasOutOfOrderBoundary = false;
|
|
3447
3459
|
const slotContext = createSlotRenderContext(options.slots);
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
const rendered = await renderShellPrefixSuffix(
|
|
3460
|
+
const renderShell = (shell: ShellFile) =>
|
|
3461
|
+
renderShellPrefixSuffix(
|
|
3451
3462
|
options.appDir,
|
|
3452
3463
|
shell,
|
|
3453
3464
|
options.props,
|
|
@@ -3461,6 +3472,18 @@ async function applyLayouts(options: {
|
|
|
3461
3472
|
options.vitePlugins,
|
|
3462
3473
|
options.importPolicy,
|
|
3463
3474
|
);
|
|
3475
|
+
const renderedShells =
|
|
3476
|
+
Object.keys(slotContext.namedSlots).length === 0
|
|
3477
|
+
? await Promise.all(layoutFiles.map(renderShell))
|
|
3478
|
+
: undefined;
|
|
3479
|
+
|
|
3480
|
+
for (let index = layoutFiles.length - 1; index >= 0; index -= 1) {
|
|
3481
|
+
const shell = layoutFiles[index];
|
|
3482
|
+
if (shell === undefined) {
|
|
3483
|
+
continue;
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
const rendered = renderedShells?.[index] ?? (await renderShell(shell));
|
|
3464
3487
|
shellHasOutOfOrderBoundary ||= rendered.hasOutOfOrderBoundary;
|
|
3465
3488
|
html = `${rendered.prefix}${html}${rendered.suffix}`;
|
|
3466
3489
|
}
|
|
@@ -3497,12 +3520,9 @@ async function layoutShellsForPage(
|
|
|
3497
3520
|
importPolicy?: AppRouterImportPolicy | undefined,
|
|
3498
3521
|
): Promise<RenderedShell[]> {
|
|
3499
3522
|
const layoutFiles = await shellFilesForPage(appDir, pageFile, serverModuleCacheVersion);
|
|
3500
|
-
const shells: RenderedShell[] = [];
|
|
3501
3523
|
const slotContext = createSlotRenderContext(slots);
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
shells.push(
|
|
3505
|
-
await renderShellPrefixSuffix(
|
|
3524
|
+
const renderShell = (shell: ShellFile) =>
|
|
3525
|
+
renderShellPrefixSuffix(
|
|
3506
3526
|
appDir,
|
|
3507
3527
|
shell,
|
|
3508
3528
|
props,
|
|
@@ -3515,8 +3535,16 @@ async function layoutShellsForPage(
|
|
|
3515
3535
|
define,
|
|
3516
3536
|
vitePlugins,
|
|
3517
3537
|
importPolicy,
|
|
3518
|
-
)
|
|
3519
|
-
|
|
3538
|
+
);
|
|
3539
|
+
const shells =
|
|
3540
|
+
Object.keys(slotContext.namedSlots).length === 0
|
|
3541
|
+
? await Promise.all(layoutFiles.map(renderShell))
|
|
3542
|
+
: [];
|
|
3543
|
+
|
|
3544
|
+
if (shells.length === 0 && layoutFiles.length > 0) {
|
|
3545
|
+
for (const shell of layoutFiles) {
|
|
3546
|
+
shells.push(await renderShell(shell));
|
|
3547
|
+
}
|
|
3520
3548
|
}
|
|
3521
3549
|
|
|
3522
3550
|
warnUnconsumedRouteSlots({
|
|
@@ -3559,18 +3587,94 @@ async function renderShellPrefixSuffix(
|
|
|
3559
3587
|
}
|
|
3560
3588
|
}
|
|
3561
3589
|
|
|
3590
|
+
const staticEntry =
|
|
3591
|
+
cacheKey === undefined ? undefined : shellStaticRenderCache.get(cacheKey);
|
|
3592
|
+
const loadedStaticEntry =
|
|
3593
|
+
staticEntry ??
|
|
3594
|
+
(await loadShellStaticRenderEntry({
|
|
3595
|
+
cacheKey,
|
|
3596
|
+
clientRouteInferenceCache,
|
|
3597
|
+
define,
|
|
3598
|
+
importPolicy,
|
|
3599
|
+
serverModuleCacheVersion,
|
|
3600
|
+
serverModules,
|
|
3601
|
+
serverSourceFiles,
|
|
3602
|
+
shell,
|
|
3603
|
+
timing,
|
|
3604
|
+
vitePlugins,
|
|
3605
|
+
}));
|
|
3606
|
+
|
|
3562
3607
|
let phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
3563
|
-
const
|
|
3564
|
-
|
|
3608
|
+
const layoutHtml = loadedStaticEntry.shellUsesAwait
|
|
3609
|
+
? await renderShellStreamComponent(loadedStaticEntry.component as StreamComponent, props)
|
|
3610
|
+
: await (loadedStaticEntry.component as ServerComponent)(props);
|
|
3611
|
+
addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutComponentRenderMs");
|
|
3565
3612
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
3613
|
+
const rendered = {
|
|
3614
|
+
...splitLayoutSlot(markShellBoundary(layoutHtml, shell), slotContext),
|
|
3615
|
+
hasOutOfOrderBoundary: loadedStaticEntry.hasOutOfOrderBoundary,
|
|
3616
|
+
};
|
|
3617
|
+
addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutSlotSplitMs");
|
|
3618
|
+
const shellCacheKey = loadedStaticEntry.shellUsesAwait ? undefined : cacheKey;
|
|
3619
|
+
const cached =
|
|
3620
|
+
shellCacheKey !== undefined
|
|
3621
|
+
? readRouterRuntimeCacheEntry(renderedShellCache, shellCacheKey, renderedShellCacheCounters)
|
|
3622
|
+
: undefined;
|
|
3623
|
+
|
|
3624
|
+
// Detect purity: a zero-arg component cannot depend on props. The
|
|
3625
|
+
// markShellBoundary + splitLayoutSlot output is then constant for
|
|
3626
|
+
// the (appDir, shellFile, version) tuple. We only set the cache
|
|
3627
|
+
// entry on the first request that observes the function arity; on
|
|
3628
|
+
// an "impure" tag we never overwrite it.
|
|
3629
|
+
if (shellCacheKey !== undefined && cached !== "impure") {
|
|
3630
|
+
if (loadedStaticEntry.component.length === 0) {
|
|
3631
|
+
if (renderedShellCache.size >= MAX_RENDERED_SHELL_CACHE_ENTRIES) {
|
|
3632
|
+
const oldestKey = renderedShellCache.keys().next().value;
|
|
3633
|
+
if (oldestKey !== undefined) {
|
|
3634
|
+
renderedShellCache.delete(oldestKey);
|
|
3635
|
+
renderedShellCacheCounters.evictions += 1;
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
renderedShellCache.set(shellCacheKey, rendered);
|
|
3639
|
+
} else {
|
|
3640
|
+
// Impure — stamp the cache so subsequent lookups short-circuit
|
|
3641
|
+
// without re-checking arity. We still run the per-request
|
|
3642
|
+
// render path above so the props are honoured.
|
|
3643
|
+
renderedShellCache.set(shellCacheKey, "impure");
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
return rendered;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
async function loadShellStaticRenderEntry(options: {
|
|
3651
|
+
cacheKey: string | undefined;
|
|
3652
|
+
clientRouteInferenceCache?: ClientRouteInferenceCache | undefined;
|
|
3653
|
+
define?: UserConfig["define"] | undefined;
|
|
3654
|
+
importPolicy?: AppRouterImportPolicy | undefined;
|
|
3655
|
+
serverModuleCacheVersion: string | undefined;
|
|
3656
|
+
serverModules: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
|
|
3657
|
+
serverSourceFiles: ReadonlyMap<string, string> | undefined;
|
|
3658
|
+
shell: ShellFile;
|
|
3659
|
+
timing?: RenderTiming | undefined;
|
|
3660
|
+
vitePlugins?: readonly PluginOption[] | undefined;
|
|
3661
|
+
}): Promise<ShellStaticRenderEntry> {
|
|
3662
|
+
let phaseStartedAt = renderTimingPhaseStartedAt(options.timing);
|
|
3663
|
+
const code = await readServerSourceFile(
|
|
3664
|
+
options.shell.file,
|
|
3665
|
+
options.serverModuleCacheVersion,
|
|
3666
|
+
options.serverSourceFiles,
|
|
3667
|
+
);
|
|
3668
|
+
addRenderTimingPhaseDuration(options.timing, phaseStartedAt, "layoutSourceReadMs");
|
|
3669
|
+
phaseStartedAt = renderTimingPhaseStartedAt(options.timing);
|
|
3566
3670
|
const shellUsesAwait = await mayRenderOutOfOrderBoundaryDeep({
|
|
3567
3671
|
code,
|
|
3568
|
-
filename: shell.file,
|
|
3569
|
-
serverModuleCacheVersion,
|
|
3570
|
-
serverSourceFiles,
|
|
3672
|
+
filename: options.shell.file,
|
|
3673
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
3674
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
3571
3675
|
});
|
|
3572
3676
|
const serverOutput: ServerOutputMode = shellUsesAwait ? "stream" : "string";
|
|
3573
|
-
const artifact = serverModules?.get(shell.file)?.[serverOutput];
|
|
3677
|
+
const artifact = options.serverModules?.get(options.shell.file)?.[serverOutput];
|
|
3574
3678
|
const clientInference =
|
|
3575
3679
|
artifact !== undefined && artifact.sourceHash === memoizedHashText(code)
|
|
3576
3680
|
? {
|
|
@@ -3580,10 +3684,10 @@ async function renderShellPrefixSuffix(
|
|
|
3580
3684
|
diagnostics: [],
|
|
3581
3685
|
}
|
|
3582
3686
|
: await inferClientRouteModule({
|
|
3583
|
-
cache: clientRouteInferenceCache,
|
|
3584
|
-
code: stripRouteClientOnlyExports(code, shell.file),
|
|
3585
|
-
filename: shell.file,
|
|
3586
|
-
vitePlugins,
|
|
3687
|
+
cache: options.clientRouteInferenceCache,
|
|
3688
|
+
code: stripRouteClientOnlyExports(code, options.shell.file),
|
|
3689
|
+
filename: options.shell.file,
|
|
3690
|
+
vitePlugins: options.vitePlugins,
|
|
3587
3691
|
});
|
|
3588
3692
|
for (const diagnostic of clientInference.diagnostics) {
|
|
3589
3693
|
console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
|
|
@@ -3592,81 +3696,57 @@ async function renderShellPrefixSuffix(
|
|
|
3592
3696
|
code,
|
|
3593
3697
|
clientBoundaryImports: clientInference.clientBoundaryImports,
|
|
3594
3698
|
clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
|
|
3595
|
-
filename: shell.file,
|
|
3596
|
-
serverModules,
|
|
3699
|
+
filename: options.shell.file,
|
|
3700
|
+
serverModules: options.serverModules,
|
|
3597
3701
|
serverOutput,
|
|
3598
3702
|
});
|
|
3599
|
-
addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutTransformMs");
|
|
3703
|
+
addRenderTimingPhaseDuration(options.timing, phaseStartedAt, "layoutTransformMs");
|
|
3600
3704
|
const fatalDiagnostics = fatalServerDiagnostics(output.diagnostics);
|
|
3601
3705
|
|
|
3602
3706
|
if (fatalDiagnostics.length > 0) {
|
|
3603
|
-
throw new Error(formatServerDiagnostics(shell.file, fatalDiagnostics));
|
|
3707
|
+
throw new Error(formatServerDiagnostics(options.shell.file, fatalDiagnostics));
|
|
3604
3708
|
}
|
|
3605
3709
|
|
|
3606
|
-
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
3710
|
+
phaseStartedAt = renderTimingPhaseStartedAt(options.timing);
|
|
3607
3711
|
const component = shellUsesAwait
|
|
3608
3712
|
? selectStreamComponent(
|
|
3609
3713
|
await loadServerStreamModule(
|
|
3610
3714
|
output.code,
|
|
3611
|
-
shell.file,
|
|
3612
|
-
serverModules,
|
|
3613
|
-
serverModuleCacheVersion,
|
|
3614
|
-
define,
|
|
3615
|
-
vitePlugins,
|
|
3616
|
-
importPolicy,
|
|
3715
|
+
options.shell.file,
|
|
3716
|
+
options.serverModules,
|
|
3717
|
+
options.serverModuleCacheVersion,
|
|
3718
|
+
options.define,
|
|
3719
|
+
options.vitePlugins,
|
|
3720
|
+
options.importPolicy,
|
|
3617
3721
|
),
|
|
3618
3722
|
)
|
|
3619
3723
|
: await loadServerComponent(
|
|
3620
3724
|
output.code,
|
|
3621
|
-
shell.file,
|
|
3622
|
-
serverModules,
|
|
3623
|
-
serverModuleCacheVersion,
|
|
3624
|
-
define,
|
|
3625
|
-
vitePlugins,
|
|
3626
|
-
importPolicy,
|
|
3725
|
+
options.shell.file,
|
|
3726
|
+
options.serverModules,
|
|
3727
|
+
options.serverModuleCacheVersion,
|
|
3728
|
+
options.define,
|
|
3729
|
+
options.vitePlugins,
|
|
3730
|
+
options.importPolicy,
|
|
3627
3731
|
);
|
|
3628
|
-
addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutModuleLoadMs");
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
? await renderShellStreamComponent(component as StreamComponent, props)
|
|
3632
|
-
: await (component as ServerComponent)(props);
|
|
3633
|
-
addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutComponentRenderMs");
|
|
3634
|
-
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
3635
|
-
const rendered = {
|
|
3636
|
-
...splitLayoutSlot(markShellBoundary(layoutHtml, shell), slotContext),
|
|
3732
|
+
addRenderTimingPhaseDuration(options.timing, phaseStartedAt, "layoutModuleLoadMs");
|
|
3733
|
+
const entry = {
|
|
3734
|
+
component,
|
|
3637
3735
|
hasOutOfOrderBoundary: hasOutOfOrderBoundary(output.code),
|
|
3736
|
+
shellUsesAwait,
|
|
3638
3737
|
};
|
|
3639
|
-
addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutSlotSplitMs");
|
|
3640
|
-
const shellCacheKey = shellUsesAwait ? undefined : cacheKey;
|
|
3641
|
-
const cached =
|
|
3642
|
-
shellCacheKey !== undefined
|
|
3643
|
-
? readRouterRuntimeCacheEntry(renderedShellCache, shellCacheKey, renderedShellCacheCounters)
|
|
3644
|
-
: undefined;
|
|
3645
3738
|
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
if (shellCacheKey !== undefined && cached !== "impure") {
|
|
3652
|
-
if (component.length === 0) {
|
|
3653
|
-
if (renderedShellCache.size >= MAX_RENDERED_SHELL_CACHE_ENTRIES) {
|
|
3654
|
-
const oldestKey = renderedShellCache.keys().next().value;
|
|
3655
|
-
if (oldestKey !== undefined) {
|
|
3656
|
-
renderedShellCache.delete(oldestKey);
|
|
3657
|
-
renderedShellCacheCounters.evictions += 1;
|
|
3658
|
-
}
|
|
3739
|
+
if (options.cacheKey !== undefined) {
|
|
3740
|
+
if (shellStaticRenderCache.size >= MAX_SHELL_STATIC_RENDER_CACHE_ENTRIES) {
|
|
3741
|
+
const oldestKey = shellStaticRenderCache.keys().next().value;
|
|
3742
|
+
if (oldestKey !== undefined) {
|
|
3743
|
+
shellStaticRenderCache.delete(oldestKey);
|
|
3659
3744
|
}
|
|
3660
|
-
renderedShellCache.set(shellCacheKey, rendered);
|
|
3661
|
-
} else {
|
|
3662
|
-
// Impure — stamp the cache so subsequent lookups short-circuit
|
|
3663
|
-
// without re-checking arity. We still run the per-request
|
|
3664
|
-
// render path above so the props are honoured.
|
|
3665
|
-
renderedShellCache.set(shellCacheKey, "impure");
|
|
3666
3745
|
}
|
|
3746
|
+
shellStaticRenderCache.set(options.cacheKey, entry);
|
|
3667
3747
|
}
|
|
3668
3748
|
|
|
3669
|
-
return
|
|
3749
|
+
return entry;
|
|
3670
3750
|
}
|
|
3671
3751
|
|
|
3672
3752
|
async function renderShellStreamComponent(
|
|
@@ -3691,14 +3771,31 @@ async function renderShellStreamComponent(
|
|
|
3691
3771
|
// next request.
|
|
3692
3772
|
const shellFilesCache = new Map<string, ShellFile[]>();
|
|
3693
3773
|
const MAX_SHELL_FILES_CACHE_ENTRIES = 1024;
|
|
3774
|
+
const devShellFilesCache = new Map<string, { directoryStats: readonly FileStat[]; files: ShellFile[] }>();
|
|
3775
|
+
const shellStaticRenderCache = new Map<string, ShellStaticRenderEntry>();
|
|
3776
|
+
const MAX_SHELL_STATIC_RENDER_CACHE_ENTRIES = 1024;
|
|
3694
3777
|
const routeMiddlewareControlCache = new Map<string, Promise<RouteMiddlewareControl | undefined>>();
|
|
3695
3778
|
const MAX_ROUTE_MIDDLEWARE_CONTROL_CACHE_ENTRIES = 1024;
|
|
3696
3779
|
const appMiddlewareFileCache = new Map<string, Promise<boolean>>();
|
|
3697
3780
|
const MAX_APP_MIDDLEWARE_FILE_CACHE_ENTRIES = 1024;
|
|
3781
|
+
const devAppMiddlewareFileCache = new Map<string, { directoryStat: FileStat; hasMiddleware: boolean }>();
|
|
3698
3782
|
const routeMiddlewareControlSourceCache = new Map<
|
|
3699
3783
|
string,
|
|
3700
3784
|
{ control: RouteMiddlewareControl | undefined; mtimeMs: number }
|
|
3701
3785
|
>();
|
|
3786
|
+
const devServerSourceFileCache = new Map<string, { mtimeMs: number; size: number; source: string }>();
|
|
3787
|
+
const MAX_DEV_SERVER_SOURCE_FILE_CACHE_ENTRIES = 2048;
|
|
3788
|
+
|
|
3789
|
+
interface FileStat {
|
|
3790
|
+
mtimeMs: number;
|
|
3791
|
+
size: number;
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
interface ShellStaticRenderEntry {
|
|
3795
|
+
component: ServerComponent | StreamComponent;
|
|
3796
|
+
hasOutOfOrderBoundary: boolean;
|
|
3797
|
+
shellUsesAwait: boolean;
|
|
3798
|
+
}
|
|
3702
3799
|
|
|
3703
3800
|
async function shellFilesForPage(
|
|
3704
3801
|
appDir: string,
|
|
@@ -3716,6 +3813,36 @@ async function shellFilesForPage(
|
|
|
3716
3813
|
}
|
|
3717
3814
|
}
|
|
3718
3815
|
|
|
3816
|
+
const devCacheKey = cacheKey === undefined ? `${appDir}\0${pageFile}` : undefined;
|
|
3817
|
+
if (devCacheKey !== undefined) {
|
|
3818
|
+
const directoryStats = await routeShellDirectoryStats(appDir, pageFile);
|
|
3819
|
+
const cached = devShellFilesCache.get(devCacheKey);
|
|
3820
|
+
if (
|
|
3821
|
+
cached !== undefined &&
|
|
3822
|
+
sameFileStats(cached.directoryStats, directoryStats)
|
|
3823
|
+
) {
|
|
3824
|
+
return cached.files;
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
const files = await shellFilesForPageUncached(appDir, pageFile);
|
|
3828
|
+
devShellFilesCache.set(devCacheKey, { directoryStats, files });
|
|
3829
|
+
return files;
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
const files = await shellFilesForPageUncached(appDir, pageFile);
|
|
3833
|
+
if (cacheKey !== undefined) {
|
|
3834
|
+
if (shellFilesCache.size >= MAX_SHELL_FILES_CACHE_ENTRIES) {
|
|
3835
|
+
const oldestKey = shellFilesCache.keys().next().value;
|
|
3836
|
+
if (oldestKey !== undefined) {
|
|
3837
|
+
shellFilesCache.delete(oldestKey);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
shellFilesCache.set(cacheKey, files);
|
|
3841
|
+
}
|
|
3842
|
+
return files;
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
async function shellFilesForPageUncached(appDir: string, pageFile: string): Promise<ShellFile[]> {
|
|
3719
3846
|
const shells = await existingRouteShellCandidates(appDir, pageFile, async (file) => {
|
|
3720
3847
|
try {
|
|
3721
3848
|
await access(file);
|
|
@@ -3730,15 +3857,6 @@ async function shellFilesForPage(
|
|
|
3730
3857
|
kind: shell.kind,
|
|
3731
3858
|
}));
|
|
3732
3859
|
|
|
3733
|
-
if (cacheKey !== undefined) {
|
|
3734
|
-
if (shellFilesCache.size >= MAX_SHELL_FILES_CACHE_ENTRIES) {
|
|
3735
|
-
const oldestKey = shellFilesCache.keys().next().value;
|
|
3736
|
-
if (oldestKey !== undefined) {
|
|
3737
|
-
shellFilesCache.delete(oldestKey);
|
|
3738
|
-
}
|
|
3739
|
-
}
|
|
3740
|
-
shellFilesCache.set(cacheKey, files);
|
|
3741
|
-
}
|
|
3742
3860
|
return files;
|
|
3743
3861
|
}
|
|
3744
3862
|
|
|
@@ -3779,22 +3897,33 @@ async function hasAppMiddleware(options: {
|
|
|
3779
3897
|
}
|
|
3780
3898
|
}
|
|
3781
3899
|
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3900
|
+
if (cacheKey === undefined) {
|
|
3901
|
+
const directoryStat = await fileStat(options.appDir);
|
|
3902
|
+
const cached = devAppMiddlewareFileCache.get(options.appDir);
|
|
3903
|
+
if (
|
|
3904
|
+
cached !== undefined &&
|
|
3905
|
+
sameFileStat(cached.directoryStat, directoryStat)
|
|
3906
|
+
) {
|
|
3907
|
+
return cached.hasMiddleware;
|
|
3785
3908
|
}
|
|
3909
|
+
|
|
3910
|
+
const hasMiddleware = await hasAppMiddlewareUncached(middlewareFiles);
|
|
3911
|
+
devAppMiddlewareFileCache.set(options.appDir, { directoryStat, hasMiddleware });
|
|
3912
|
+
return hasMiddleware;
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
const loaded = hasAppMiddlewareUncached(middlewareFiles).catch((error) => {
|
|
3916
|
+
appMiddlewareFileCache.delete(cacheKey);
|
|
3786
3917
|
throw error;
|
|
3787
3918
|
});
|
|
3788
3919
|
|
|
3789
|
-
if (
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
appMiddlewareFileCache.delete(oldestKey);
|
|
3794
|
-
}
|
|
3920
|
+
if (appMiddlewareFileCache.size >= MAX_APP_MIDDLEWARE_FILE_CACHE_ENTRIES) {
|
|
3921
|
+
const oldestKey = appMiddlewareFileCache.keys().next().value;
|
|
3922
|
+
if (oldestKey !== undefined) {
|
|
3923
|
+
appMiddlewareFileCache.delete(oldestKey);
|
|
3795
3924
|
}
|
|
3796
|
-
appMiddlewareFileCache.set(cacheKey, loaded);
|
|
3797
3925
|
}
|
|
3926
|
+
appMiddlewareFileCache.set(cacheKey, loaded);
|
|
3798
3927
|
|
|
3799
3928
|
return loaded;
|
|
3800
3929
|
}
|
|
@@ -3812,6 +3941,57 @@ async function hasAppMiddlewareUncached(files: readonly string[]): Promise<boole
|
|
|
3812
3941
|
return false;
|
|
3813
3942
|
}
|
|
3814
3943
|
|
|
3944
|
+
async function routeShellDirectoryStats(appDir: string, pageFile: string): Promise<FileStat[]> {
|
|
3945
|
+
const directories: string[] = [];
|
|
3946
|
+
let current = dirname(pageFile);
|
|
3947
|
+
const resolvedAppDir = resolve(appDir);
|
|
3948
|
+
|
|
3949
|
+
while (true) {
|
|
3950
|
+
directories.push(current);
|
|
3951
|
+
if (resolve(current) === resolvedAppDir) {
|
|
3952
|
+
break;
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
const parent = dirname(current);
|
|
3956
|
+
if (parent === current) {
|
|
3957
|
+
break;
|
|
3958
|
+
}
|
|
3959
|
+
current = parent;
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
return await Promise.all(directories.map((directory) => fileStatOrMissing(directory)));
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
async function fileStat(file: string): Promise<FileStat> {
|
|
3966
|
+
const stats = await stat(file);
|
|
3967
|
+
return { mtimeMs: stats.mtimeMs, size: stats.size };
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
async function fileStatOrMissing(file: string): Promise<FileStat> {
|
|
3971
|
+
try {
|
|
3972
|
+
return await fileStat(file);
|
|
3973
|
+
} catch (error) {
|
|
3974
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
3975
|
+
return { mtimeMs: -1, size: -1 };
|
|
3976
|
+
}
|
|
3977
|
+
throw error;
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
function sameFileStats(left: readonly FileStat[], right: readonly FileStat[]): boolean {
|
|
3982
|
+
return (
|
|
3983
|
+
left.length === right.length &&
|
|
3984
|
+
left.every((value, index) => {
|
|
3985
|
+
const other = right[index];
|
|
3986
|
+
return other !== undefined && sameFileStat(value, other);
|
|
3987
|
+
})
|
|
3988
|
+
);
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
function sameFileStat(left: FileStat, right: FileStat): boolean {
|
|
3992
|
+
return left.mtimeMs === right.mtimeMs && left.size === right.size;
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3815
3995
|
async function loadRouteMiddlewareControl(options: {
|
|
3816
3996
|
appDir: string;
|
|
3817
3997
|
pageFile: string;
|
|
@@ -4335,10 +4515,9 @@ async function loadComposedRouteMetadataUncached(options: {
|
|
|
4335
4515
|
);
|
|
4336
4516
|
const metadata: RouteMetadata[] = [];
|
|
4337
4517
|
let dynamic = hasGenerateMetadataExport(options.code);
|
|
4338
|
-
|
|
4339
|
-
for (const shell of layoutFiles) {
|
|
4518
|
+
const layoutMetadataTasks = layoutFiles.map(async (shell) => {
|
|
4340
4519
|
if (shell.kind !== "layout") {
|
|
4341
|
-
|
|
4520
|
+
return { code: undefined, metadata: undefined };
|
|
4342
4521
|
}
|
|
4343
4522
|
|
|
4344
4523
|
const code = await readServerSourceFile(
|
|
@@ -4346,7 +4525,6 @@ async function loadComposedRouteMetadataUncached(options: {
|
|
|
4346
4525
|
options.serverModuleCacheVersion,
|
|
4347
4526
|
options.serverSourceFiles,
|
|
4348
4527
|
);
|
|
4349
|
-
dynamic ||= hasGenerateMetadataExport(code);
|
|
4350
4528
|
const shellMetadata = await loadRouteMetadata({
|
|
4351
4529
|
appDir: options.appDir,
|
|
4352
4530
|
code,
|
|
@@ -4359,12 +4537,9 @@ async function loadComposedRouteMetadataUncached(options: {
|
|
|
4359
4537
|
vitePlugins: options.vitePlugins,
|
|
4360
4538
|
});
|
|
4361
4539
|
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
}
|
|
4366
|
-
|
|
4367
|
-
const pageMetadata = await loadRouteMetadata({
|
|
4540
|
+
return { code, metadata: shellMetadata };
|
|
4541
|
+
});
|
|
4542
|
+
const pageMetadataTask = loadRouteMetadata({
|
|
4368
4543
|
appDir: options.appDir,
|
|
4369
4544
|
code: options.code,
|
|
4370
4545
|
context: options.context,
|
|
@@ -4376,6 +4551,17 @@ async function loadComposedRouteMetadataUncached(options: {
|
|
|
4376
4551
|
vitePlugins: options.vitePlugins,
|
|
4377
4552
|
});
|
|
4378
4553
|
|
|
4554
|
+
for (const task of layoutMetadataTasks) {
|
|
4555
|
+
const shell = await task;
|
|
4556
|
+
if (shell.code !== undefined) {
|
|
4557
|
+
dynamic ||= hasGenerateMetadataExport(shell.code);
|
|
4558
|
+
}
|
|
4559
|
+
if (shell.metadata !== undefined) {
|
|
4560
|
+
metadata.push(shell.metadata);
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
|
|
4564
|
+
const pageMetadata = await pageMetadataTask;
|
|
4379
4565
|
if (pageMetadata !== undefined) {
|
|
4380
4566
|
metadata.push(pageMetadata);
|
|
4381
4567
|
}
|
|
@@ -4632,7 +4818,7 @@ function readServerSourceFile(
|
|
|
4632
4818
|
}
|
|
4633
4819
|
|
|
4634
4820
|
if (serverModuleCacheVersion === undefined) {
|
|
4635
|
-
return
|
|
4821
|
+
return readDevServerSourceFile(file);
|
|
4636
4822
|
}
|
|
4637
4823
|
|
|
4638
4824
|
const key = `${serverModuleCacheVersion}:${file}`;
|
|
@@ -4661,6 +4847,28 @@ function readServerSourceFile(
|
|
|
4661
4847
|
return loaded;
|
|
4662
4848
|
}
|
|
4663
4849
|
|
|
4850
|
+
async function readDevServerSourceFile(file: string): Promise<string> {
|
|
4851
|
+
const stats = await fileStat(file);
|
|
4852
|
+
const cached = devServerSourceFileCache.get(file);
|
|
4853
|
+
if (
|
|
4854
|
+
cached !== undefined &&
|
|
4855
|
+
cached.mtimeMs === stats.mtimeMs &&
|
|
4856
|
+
cached.size === stats.size
|
|
4857
|
+
) {
|
|
4858
|
+
return cached.source;
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
const source = await readFile(file, "utf8");
|
|
4862
|
+
if (devServerSourceFileCache.size >= MAX_DEV_SERVER_SOURCE_FILE_CACHE_ENTRIES) {
|
|
4863
|
+
const oldestKey = devServerSourceFileCache.keys().next().value;
|
|
4864
|
+
if (oldestKey !== undefined) {
|
|
4865
|
+
devServerSourceFileCache.delete(oldestKey);
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
devServerSourceFileCache.set(file, { ...stats, source });
|
|
4869
|
+
return source;
|
|
4870
|
+
}
|
|
4871
|
+
|
|
4664
4872
|
function setBoundedCacheEntry<K, V>(
|
|
4665
4873
|
cache: Map<K, V>,
|
|
4666
4874
|
key: K,
|