@reckona/mreact-router 0.0.1
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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/actions.d.ts +43 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +577 -0
- package/dist/actions.js.map +1 -0
- package/dist/adapters/aws-lambda.d.ts +45 -0
- package/dist/adapters/aws-lambda.d.ts.map +1 -0
- package/dist/adapters/aws-lambda.js +168 -0
- package/dist/adapters/aws-lambda.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +94 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -0
- package/dist/adapters/cloudflare.js +390 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/devtools.d.ts +4 -0
- package/dist/adapters/devtools.d.ts.map +1 -0
- package/dist/adapters/devtools.js +5 -0
- package/dist/adapters/devtools.js.map +1 -0
- package/dist/adapters/edge.d.ts +9 -0
- package/dist/adapters/edge.d.ts.map +1 -0
- package/dist/adapters/edge.js +53 -0
- package/dist/adapters/edge.js.map +1 -0
- package/dist/adapters/node.d.ts +26 -0
- package/dist/adapters/node.d.ts.map +1 -0
- package/dist/adapters/node.js +64 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/static.d.ts +10 -0
- package/dist/adapters/static.d.ts.map +1 -0
- package/dist/adapters/static.js +34 -0
- package/dist/adapters/static.js.map +1 -0
- package/dist/assets.d.ts +18 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +67 -0
- package/dist/assets.js.map +1 -0
- package/dist/build.d.ts +36 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +322 -0
- package/dist/build.js.map +1 -0
- package/dist/cache.d.ts +54 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +221 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +37 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +105 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1268 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/cookies.d.ts +14 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/cookies.js +69 -0
- package/dist/cookies.js.map +1 -0
- package/dist/csp.d.ts +6 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +70 -0
- package/dist/csp.js.map +1 -0
- package/dist/dev-server.d.ts +16 -0
- package/dist/dev-server.d.ts.map +1 -0
- package/dist/dev-server.js +103 -0
- package/dist/dev-server.js.map +1 -0
- package/dist/http.d.ts +23 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +106 -0
- package/dist/http.js.map +1 -0
- package/dist/i18n.d.ts +15 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +61 -0
- package/dist/i18n.js.map +1 -0
- package/dist/import-policy.d.ts +30 -0
- package/dist/import-policy.d.ts.map +1 -0
- package/dist/import-policy.js +105 -0
- package/dist/import-policy.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +47 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +60 -0
- package/dist/logger.js.map +1 -0
- package/dist/module-runner.d.ts +9 -0
- package/dist/module-runner.d.ts.map +1 -0
- package/dist/module-runner.js +112 -0
- package/dist/module-runner.js.map +1 -0
- package/dist/native-escape.d.ts +2 -0
- package/dist/native-escape.d.ts.map +1 -0
- package/dist/native-escape.js +43 -0
- package/dist/native-escape.js.map +1 -0
- package/dist/native-route-matcher.d.ts +5 -0
- package/dist/native-route-matcher.d.ts.map +1 -0
- package/dist/native-route-matcher.js +91 -0
- package/dist/native-route-matcher.js.map +1 -0
- package/dist/navigation.d.ts +25 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +125 -0
- package/dist/navigation.js.map +1 -0
- package/dist/prerender-store.d.ts +37 -0
- package/dist/prerender-store.d.ts.map +1 -0
- package/dist/prerender-store.js +158 -0
- package/dist/prerender-store.js.map +1 -0
- package/dist/render.d.ts +26 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1688 -0
- package/dist/render.js.map +1 -0
- package/dist/route-path.d.ts +2 -0
- package/dist/route-path.d.ts.map +1 -0
- package/dist/route-path.js +5 -0
- package/dist/route-path.js.map +1 -0
- package/dist/route-source.d.ts +9 -0
- package/dist/route-source.d.ts.map +1 -0
- package/dist/route-source.js +44 -0
- package/dist/route-source.js.map +1 -0
- package/dist/routes.d.ts +38 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +168 -0
- package/dist/routes.js.map +1 -0
- package/dist/serve.d.ts +63 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +445 -0
- package/dist/serve.js.map +1 -0
- package/dist/session.d.ts +25 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +104 -0
- package/dist/session.js.map +1 -0
- package/dist/vite-config.d.ts +8 -0
- package/dist/vite-config.d.ts.map +1 -0
- package/dist/vite-config.js +17 -0
- package/dist/vite-config.js.map +1 -0
- package/dist/vite.d.ts +25 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +150 -0
- package/dist/vite.js.map +1 -0
- package/package.json +91 -0
package/dist/render.js
ADDED
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { access, readFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
5
|
+
import { transform, } from "@reckona/mreact-compiler";
|
|
6
|
+
import { createQueryClient, dehydrate, __MREACT_QUERY_STATE_SCRIPT_ID, runWithQueryClient, } from "@reckona/mreact-query";
|
|
7
|
+
import { build as bundle } from "esbuild";
|
|
8
|
+
import { createStringSink, renderAsyncBoundary, renderOutOfOrderReorderScript, renderToReadableStream, } from "@reckona/mreact-server";
|
|
9
|
+
import { hydrationMarkerParts, inferClientRouteModule, withHydrationMarkers, withRouteMarkers, } from "./client.js";
|
|
10
|
+
import { assetPath } from "./assets.js";
|
|
11
|
+
import { escapeHtmlAttribute, escapeHtmlText as escapeHtml, } from "@reckona/mreact-shared/html-escape";
|
|
12
|
+
import { matchRoute, scanAppRoutes } from "./routes.js";
|
|
13
|
+
import { dispatchServerActionRequest, prepareRouteServerActions, serverActionCookie, } from "./actions.js";
|
|
14
|
+
import { beginRouteCacheContext, cachedRouteResponse, cacheRouteResponse, routeCacheKey, routeCachePolicyFromSource, } from "./cache.js";
|
|
15
|
+
import { importAppRouterFileModule, importAppRouterSourceModule } from "./module-runner.js";
|
|
16
|
+
import { contentSecurityPolicy } from "./csp.js";
|
|
17
|
+
import { htmlResponse } from "./http.js";
|
|
18
|
+
import { isNotFoundError, isRedirectError, rewriteLocation } from "./navigation.js";
|
|
19
|
+
import { createAppRouterImportPolicyPlugin } from "./import-policy.js";
|
|
20
|
+
import { hasLoaderExport, isStreamRouteSource, stripRouteModuleExports } from "./route-source.js";
|
|
21
|
+
const nativeEscapeTransform = {
|
|
22
|
+
batchImportName: "escapeHtmlBatch",
|
|
23
|
+
batchImportSource: "@reckona/mreact-router/internal/native-escape",
|
|
24
|
+
};
|
|
25
|
+
const authRuntimeStateKey = "__mreactAuthRuntimeState";
|
|
26
|
+
const authSessionScriptId = "__mreact_auth_session";
|
|
27
|
+
const serverTransformCache = new Map();
|
|
28
|
+
const serverSourceFileCache = new Map();
|
|
29
|
+
const routeSourceAnalysisCache = new Map();
|
|
30
|
+
const composedRouteMetadataCache = new Map();
|
|
31
|
+
const maxServerTransformCacheEntries = 512;
|
|
32
|
+
const maxServerSourceFileCacheEntries = 512;
|
|
33
|
+
const maxRouteSourceAnalysisCacheEntries = 512;
|
|
34
|
+
const maxComposedRouteMetadataCacheEntries = 512;
|
|
35
|
+
// Issue 086: per-shell prefix/suffix cache. Pure layouts (whose
|
|
36
|
+
// exported component takes zero arguments and therefore cannot
|
|
37
|
+
// depend on the request props) produce the same HTML for every
|
|
38
|
+
// request, so we cache the already-split { prefix, suffix } strings
|
|
39
|
+
// keyed by appDir + shellFile + serverModuleCacheVersion. Impure
|
|
40
|
+
// layouts (function.length > 0) are tagged "impure" so we skip the
|
|
41
|
+
// detection on subsequent requests but still render per-request.
|
|
42
|
+
//
|
|
43
|
+
// The cache is only active when a version is present (production
|
|
44
|
+
// builds); dev mode keeps the previous behaviour so reloads pick up
|
|
45
|
+
// edits without server restart.
|
|
46
|
+
const renderedShellCache = new Map();
|
|
47
|
+
const MAX_RENDERED_SHELL_CACHE_ENTRIES = 1024;
|
|
48
|
+
export async function renderAppRequest(options) {
|
|
49
|
+
const authStorage = authRequestStorage();
|
|
50
|
+
if (authStorage.getStore() === undefined) {
|
|
51
|
+
return authStorage.run({}, () => renderAppRequest(options));
|
|
52
|
+
}
|
|
53
|
+
const routes = options.routes ?? (await scanAppRoutes({ appDir: options.appDir }));
|
|
54
|
+
const url = new URL(options.request.url);
|
|
55
|
+
const middlewareResponse = options.skipMiddleware === true
|
|
56
|
+
? undefined
|
|
57
|
+
: await runMiddleware({
|
|
58
|
+
appDir: options.appDir,
|
|
59
|
+
importPolicy: options.importPolicy,
|
|
60
|
+
request: options.request,
|
|
61
|
+
});
|
|
62
|
+
if (middlewareResponse !== undefined) {
|
|
63
|
+
const location = rewriteLocation(middlewareResponse);
|
|
64
|
+
if (location !== undefined) {
|
|
65
|
+
const rewriteUrl = new URL(location, options.request.url);
|
|
66
|
+
return renderAppRequest({
|
|
67
|
+
...options,
|
|
68
|
+
request: new Request(rewriteUrl, options.request),
|
|
69
|
+
skipMiddleware: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return middlewareResponse;
|
|
73
|
+
}
|
|
74
|
+
if (url.pathname === "/_mreact/actions") {
|
|
75
|
+
return dispatchServerActionRequest({
|
|
76
|
+
appDir: options.appDir,
|
|
77
|
+
importPolicy: options.importPolicy,
|
|
78
|
+
request: options.request,
|
|
79
|
+
routeCache: options.routeCache,
|
|
80
|
+
...(options.serverModuleCacheVersion === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { serverActionCacheVersion: options.serverModuleCacheVersion }),
|
|
83
|
+
serverActions: options.serverActions,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const matched = options.routeMatcher?.match(url.pathname) ?? matchRoute(routes, url.pathname);
|
|
87
|
+
if (matched === undefined) {
|
|
88
|
+
const notFoundFile = await nearestBoundaryFileForPath({
|
|
89
|
+
appDir: options.appDir,
|
|
90
|
+
filename: "not-found.mreact.tsx",
|
|
91
|
+
pathname: url.pathname,
|
|
92
|
+
});
|
|
93
|
+
return renderSpecialRoute({
|
|
94
|
+
appDir: options.appDir,
|
|
95
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
96
|
+
error: undefined,
|
|
97
|
+
request: options.request,
|
|
98
|
+
routeFile: notFoundFile,
|
|
99
|
+
serverModules: options.serverModules,
|
|
100
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
101
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
102
|
+
status: 404,
|
|
103
|
+
textFallback: "Not Found",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const queryClient = options.queryClient ?? createQueryClient();
|
|
107
|
+
let recoveryRoute;
|
|
108
|
+
let routeCacheContext;
|
|
109
|
+
try {
|
|
110
|
+
if (matched.route.kind === "server") {
|
|
111
|
+
return await dispatchServerRoute(matched.route.file, options.request);
|
|
112
|
+
}
|
|
113
|
+
// Issue 080: page routes render HTML for GET / HEAD only. Other
|
|
114
|
+
// methods (PUT, PATCH, DELETE, PROPFIND, ...) get 405 with an
|
|
115
|
+
// Allow header so the response shape complies with RFC 9110 §9
|
|
116
|
+
// and so caching intermediaries do not cross-cache method results.
|
|
117
|
+
const method = options.request.method;
|
|
118
|
+
if (method === "OPTIONS") {
|
|
119
|
+
return new Response(null, {
|
|
120
|
+
status: 204,
|
|
121
|
+
headers: { allow: "GET, HEAD, OPTIONS" },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
125
|
+
return new Response("Method Not Allowed", {
|
|
126
|
+
status: 405,
|
|
127
|
+
headers: { allow: "GET, HEAD, OPTIONS" },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
routeCacheContext = beginRouteCacheContext(options.routeCache);
|
|
131
|
+
const clientScript = options.clientScripts?.get(matched.route.path);
|
|
132
|
+
const originalCode = await readServerSourceFile(matched.route.file, options.serverModuleCacheVersion, options.serverSourceFiles);
|
|
133
|
+
const originalAnalysis = await analyzeRouteSource({
|
|
134
|
+
code: originalCode,
|
|
135
|
+
filename: matched.route.file,
|
|
136
|
+
routePath: matched.route.path,
|
|
137
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
138
|
+
});
|
|
139
|
+
const cachePolicy = originalAnalysis.cachePolicy;
|
|
140
|
+
const cacheKey = routeCacheKey(options.appDir, matched.route.path, url);
|
|
141
|
+
const mayUseRouteCache = cachePolicy === undefined
|
|
142
|
+
? originalAnalysis.usesRuntimeCacheControl
|
|
143
|
+
: cachePolicy.revalidateSeconds !== 0;
|
|
144
|
+
const cachedResponse = !mayUseRouteCache
|
|
145
|
+
? undefined
|
|
146
|
+
: await cachedRouteResponse({
|
|
147
|
+
cache: options.routeCache,
|
|
148
|
+
key: cacheKey,
|
|
149
|
+
});
|
|
150
|
+
if (cachedResponse !== undefined) {
|
|
151
|
+
return cachedResponse;
|
|
152
|
+
}
|
|
153
|
+
const preparedActions = await prepareRouteServerActions({
|
|
154
|
+
appDir: options.appDir,
|
|
155
|
+
code: originalCode,
|
|
156
|
+
pageFile: matched.route.file,
|
|
157
|
+
request: options.request,
|
|
158
|
+
});
|
|
159
|
+
const code = preparedActions.code;
|
|
160
|
+
const routeAnalysis = code === originalCode
|
|
161
|
+
? originalAnalysis
|
|
162
|
+
: await analyzeRouteSource({
|
|
163
|
+
code,
|
|
164
|
+
filename: matched.route.file,
|
|
165
|
+
routePath: matched.route.path,
|
|
166
|
+
serverModuleCacheVersion: undefined,
|
|
167
|
+
});
|
|
168
|
+
const routeCode = routeAnalysis.routeCode;
|
|
169
|
+
const streamRoute = routeAnalysis.streamRoute;
|
|
170
|
+
const clientInference = routeAnalysis.clientInference;
|
|
171
|
+
const clientRoute = clientInference.client;
|
|
172
|
+
const dataPromise = routeAnalysis.hasLoader
|
|
173
|
+
? loadRouteData({
|
|
174
|
+
code,
|
|
175
|
+
context: {
|
|
176
|
+
params: matched.params,
|
|
177
|
+
queryClient,
|
|
178
|
+
request: options.request,
|
|
179
|
+
},
|
|
180
|
+
appDir: options.appDir,
|
|
181
|
+
filename: matched.route.file,
|
|
182
|
+
importPolicy: options.importPolicy,
|
|
183
|
+
})
|
|
184
|
+
: undefined;
|
|
185
|
+
recoveryRoute = {
|
|
186
|
+
clientRoute,
|
|
187
|
+
props: {
|
|
188
|
+
params: matched.params,
|
|
189
|
+
request: { url: options.request.url },
|
|
190
|
+
},
|
|
191
|
+
routePath: matched.route.path,
|
|
192
|
+
script: clientScript,
|
|
193
|
+
};
|
|
194
|
+
if (streamRoute) {
|
|
195
|
+
const loadingFile = await nearestExistingBoundaryFileForPage({
|
|
196
|
+
appDir: options.appDir,
|
|
197
|
+
filename: "loading.mreact.tsx",
|
|
198
|
+
pageFile: matched.route.file,
|
|
199
|
+
});
|
|
200
|
+
const streamShellResponseHeaders = {
|
|
201
|
+
"content-type": "text/html; charset=utf-8",
|
|
202
|
+
"x-mreact-stream": "1",
|
|
203
|
+
};
|
|
204
|
+
if (loadingFile === undefined && !mayRenderOutOfOrderBoundary(routeCode)) {
|
|
205
|
+
const stringOutput = transformServerModule({
|
|
206
|
+
code: routeCode,
|
|
207
|
+
clientBoundaryImports: clientInference.clientBoundaryImports,
|
|
208
|
+
filename: matched.route.file,
|
|
209
|
+
serverModules: options.serverModules,
|
|
210
|
+
serverOutput: "string",
|
|
211
|
+
});
|
|
212
|
+
const stringFatalDiagnostics = stringOutput.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
|
|
213
|
+
if (stringFatalDiagnostics.length > 0) {
|
|
214
|
+
return new Response(stringFatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
|
|
215
|
+
status: 500,
|
|
216
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const data = dataPromise === undefined ? undefined : await dataPromise;
|
|
220
|
+
const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(stringOutput.code, {
|
|
221
|
+
data,
|
|
222
|
+
params: matched.params,
|
|
223
|
+
queryClient,
|
|
224
|
+
request: options.request,
|
|
225
|
+
}, matched.route.file, options.serverModules, options.serverModuleCacheVersion));
|
|
226
|
+
const pageHtml = renderedPage.html;
|
|
227
|
+
const pageHtmlForLayout = clientRoute
|
|
228
|
+
? withHydrationMarkers({
|
|
229
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
230
|
+
clientReferenceManifest: stringOutput.metadata.clientReferenceManifest,
|
|
231
|
+
html: pageHtml,
|
|
232
|
+
routePath: matched.route.path,
|
|
233
|
+
script: clientScript,
|
|
234
|
+
props: {
|
|
235
|
+
params: matched.params,
|
|
236
|
+
request: { url: options.request.url },
|
|
237
|
+
data,
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
: isNavigationRequest(options.request)
|
|
241
|
+
? withRouteMarkers({
|
|
242
|
+
html: pageHtml,
|
|
243
|
+
routePath: matched.route.path,
|
|
244
|
+
})
|
|
245
|
+
: pageHtml;
|
|
246
|
+
let html = await runWithQueryClient(queryClient, () => applyLayouts({
|
|
247
|
+
appDir: options.appDir,
|
|
248
|
+
pageFile: matched.route.file,
|
|
249
|
+
html: pageHtmlForLayout,
|
|
250
|
+
props: {
|
|
251
|
+
data,
|
|
252
|
+
params: matched.params,
|
|
253
|
+
queryClient,
|
|
254
|
+
request: options.request,
|
|
255
|
+
},
|
|
256
|
+
slots: renderedPage.slots,
|
|
257
|
+
serverModules: options.serverModules,
|
|
258
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
259
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
260
|
+
}));
|
|
261
|
+
const metadata = await loadComposedRouteMetadata({
|
|
262
|
+
appDir: options.appDir,
|
|
263
|
+
code: originalCode,
|
|
264
|
+
filename: matched.route.file,
|
|
265
|
+
importPolicy: options.importPolicy,
|
|
266
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
267
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
268
|
+
});
|
|
269
|
+
html = injectHeadMetadata(html, metadata);
|
|
270
|
+
html = injectAuthSessionClaims(html, originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined);
|
|
271
|
+
html = injectQueryState(html, dehydrate(queryClient));
|
|
272
|
+
const headers = new Headers(responseHeadersForMetadata(metadata));
|
|
273
|
+
headers.set("x-mreact-stream", "1");
|
|
274
|
+
return withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${modulePreloadTags(clientRoute ? clientScript : undefined, options.assetBaseUrl)}${html}`, { headers }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
|
|
275
|
+
}
|
|
276
|
+
const output = transformServerModule({
|
|
277
|
+
code: routeCode,
|
|
278
|
+
clientBoundaryImports: clientInference.clientBoundaryImports,
|
|
279
|
+
filename: matched.route.file,
|
|
280
|
+
serverModules: options.serverModules,
|
|
281
|
+
serverOutput: "stream",
|
|
282
|
+
serverAwaitHydration: clientRoute,
|
|
283
|
+
});
|
|
284
|
+
const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
|
|
285
|
+
if (fatalDiagnostics.length > 0) {
|
|
286
|
+
return new Response(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
|
|
287
|
+
status: 500,
|
|
288
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
if (loadingFile !== undefined) {
|
|
292
|
+
const stream = await runServerStreamModuleWithLoading(output.code, {
|
|
293
|
+
appDir: options.appDir,
|
|
294
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
295
|
+
clientRoute,
|
|
296
|
+
data: dataPromise ?? Promise.resolve(undefined),
|
|
297
|
+
loadingFile,
|
|
298
|
+
pageFile: matched.route.file,
|
|
299
|
+
params: matched.params,
|
|
300
|
+
queryClient,
|
|
301
|
+
request: options.request,
|
|
302
|
+
routePath: matched.route.path,
|
|
303
|
+
serverModules: options.serverModules,
|
|
304
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
305
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
306
|
+
script: clientScript,
|
|
307
|
+
clientReferenceManifest: output.metadata.clientReferenceManifest,
|
|
308
|
+
});
|
|
309
|
+
return withOptionalActionCookie(new Response(stream, {
|
|
310
|
+
headers: streamShellResponseHeaders,
|
|
311
|
+
}), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
|
|
312
|
+
}
|
|
313
|
+
const data = dataPromise === undefined ? undefined : await dataPromise;
|
|
314
|
+
const props = {
|
|
315
|
+
data,
|
|
316
|
+
params: matched.params,
|
|
317
|
+
queryClient,
|
|
318
|
+
request: options.request,
|
|
319
|
+
};
|
|
320
|
+
const stream = runServerStreamModule(output.code, {
|
|
321
|
+
appDir: options.appDir,
|
|
322
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
323
|
+
pageFile: matched.route.file,
|
|
324
|
+
props,
|
|
325
|
+
routePath: matched.route.path,
|
|
326
|
+
serverModules: options.serverModules,
|
|
327
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
328
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
329
|
+
clientRoute,
|
|
330
|
+
script: clientScript,
|
|
331
|
+
clientReferenceManifest: output.metadata.clientReferenceManifest,
|
|
332
|
+
});
|
|
333
|
+
return withOptionalActionCookie(new Response(stream, {
|
|
334
|
+
headers: streamShellResponseHeaders,
|
|
335
|
+
}), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
|
|
336
|
+
}
|
|
337
|
+
const output = transformServerModule({
|
|
338
|
+
code: routeCode,
|
|
339
|
+
clientBoundaryImports: clientInference.clientBoundaryImports,
|
|
340
|
+
filename: matched.route.file,
|
|
341
|
+
serverModules: options.serverModules,
|
|
342
|
+
serverOutput: "string",
|
|
343
|
+
});
|
|
344
|
+
const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
|
|
345
|
+
if (fatalDiagnostics.length > 0) {
|
|
346
|
+
return new Response(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
|
|
347
|
+
status: 500,
|
|
348
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const data = dataPromise === undefined ? undefined : await dataPromise;
|
|
352
|
+
const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(output.code, {
|
|
353
|
+
data,
|
|
354
|
+
params: matched.params,
|
|
355
|
+
queryClient,
|
|
356
|
+
request: options.request,
|
|
357
|
+
}, matched.route.file, options.serverModules, options.serverModuleCacheVersion));
|
|
358
|
+
const pageHtml = renderedPage.html;
|
|
359
|
+
// Wrap the page (not the full document) with the hydration marker so
|
|
360
|
+
// the marker sits inside <body>, not around <html>. Wrapping <html>
|
|
361
|
+
// forces the browser HTML parser to strip the wrappers and promote
|
|
362
|
+
// <head> / <body> children up to the marker, which flattens the
|
|
363
|
+
// layout into the marker and breaks the hydration target lookup.
|
|
364
|
+
const pageHtmlForLayout = clientRoute
|
|
365
|
+
? withHydrationMarkers({
|
|
366
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
367
|
+
clientReferenceManifest: output.metadata.clientReferenceManifest,
|
|
368
|
+
html: pageHtml,
|
|
369
|
+
routePath: matched.route.path,
|
|
370
|
+
script: clientScript,
|
|
371
|
+
props: {
|
|
372
|
+
params: matched.params,
|
|
373
|
+
request: { url: options.request.url },
|
|
374
|
+
data,
|
|
375
|
+
},
|
|
376
|
+
})
|
|
377
|
+
: isNavigationRequest(options.request)
|
|
378
|
+
? withRouteMarkers({
|
|
379
|
+
html: pageHtml,
|
|
380
|
+
routePath: matched.route.path,
|
|
381
|
+
})
|
|
382
|
+
: pageHtml;
|
|
383
|
+
let html = await runWithQueryClient(queryClient, () => applyLayouts({
|
|
384
|
+
appDir: options.appDir,
|
|
385
|
+
pageFile: matched.route.file,
|
|
386
|
+
html: pageHtmlForLayout,
|
|
387
|
+
props: {
|
|
388
|
+
data,
|
|
389
|
+
params: matched.params,
|
|
390
|
+
queryClient,
|
|
391
|
+
request: options.request,
|
|
392
|
+
},
|
|
393
|
+
slots: renderedPage.slots,
|
|
394
|
+
serverModules: options.serverModules,
|
|
395
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
396
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
397
|
+
}));
|
|
398
|
+
const metadata = await loadComposedRouteMetadata({
|
|
399
|
+
appDir: options.appDir,
|
|
400
|
+
code: originalCode,
|
|
401
|
+
filename: matched.route.file,
|
|
402
|
+
importPolicy: options.importPolicy,
|
|
403
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
404
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
405
|
+
});
|
|
406
|
+
html = injectHeadMetadata(html, metadata);
|
|
407
|
+
html = injectAuthSessionClaims(html, originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined);
|
|
408
|
+
html = injectQueryState(html, dehydrate(queryClient));
|
|
409
|
+
const response = withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${modulePreloadTags(clientRoute ? clientScript : undefined, options.assetBaseUrl)}${html}`, {
|
|
410
|
+
headers: responseHeadersForMetadata(metadata),
|
|
411
|
+
}), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
|
|
412
|
+
const effectiveCachePolicy = cachePolicy ?? routeCacheContext.cachePolicy;
|
|
413
|
+
return preparedActions.hasFormActions
|
|
414
|
+
? withRouteCacheHeader(response, effectiveCachePolicy)
|
|
415
|
+
: await cacheRouteResponse({
|
|
416
|
+
key: cacheKey,
|
|
417
|
+
cache: options.routeCache,
|
|
418
|
+
path: matched.route.path,
|
|
419
|
+
policy: effectiveCachePolicy,
|
|
420
|
+
response,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
if (isRedirectError(error)) {
|
|
425
|
+
return new Response(null, {
|
|
426
|
+
headers: { location: error.location },
|
|
427
|
+
status: error.status,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (isNotFoundError(error)) {
|
|
431
|
+
const notFoundFile = await nearestBoundaryFileForPage({
|
|
432
|
+
appDir: options.appDir,
|
|
433
|
+
filename: "not-found.mreact.tsx",
|
|
434
|
+
pageFile: matched.route.file,
|
|
435
|
+
});
|
|
436
|
+
return renderSpecialRoute({
|
|
437
|
+
appDir: options.appDir,
|
|
438
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
439
|
+
error: undefined,
|
|
440
|
+
request: options.request,
|
|
441
|
+
routeFile: notFoundFile,
|
|
442
|
+
serverModules: options.serverModules,
|
|
443
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
444
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
445
|
+
navigation: recoveryRoute,
|
|
446
|
+
status: 404,
|
|
447
|
+
textFallback: "Not Found",
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
const errorFile = await nearestBoundaryFileForPage({
|
|
451
|
+
appDir: options.appDir,
|
|
452
|
+
filename: "error.mreact.tsx",
|
|
453
|
+
pageFile: matched.route.file,
|
|
454
|
+
});
|
|
455
|
+
return renderSpecialRoute({
|
|
456
|
+
appDir: options.appDir,
|
|
457
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
458
|
+
error,
|
|
459
|
+
request: options.request,
|
|
460
|
+
routeFile: errorFile,
|
|
461
|
+
serverModules: options.serverModules,
|
|
462
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
463
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
464
|
+
navigation: recoveryRoute,
|
|
465
|
+
status: 500,
|
|
466
|
+
textFallback: error instanceof Error ? error.message : String(error),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
await routeCacheContext?.dispose();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function withOptionalActionCookie(response, csrfToken, csrfTokenIsNew) {
|
|
474
|
+
// Only re-issue Set-Cookie when this render minted the token. Reusing
|
|
475
|
+
// an incoming cookie value (Issue 070) means no Set-Cookie is needed
|
|
476
|
+
// and avoids stomping on a concurrent tab's hidden form input.
|
|
477
|
+
if (csrfToken !== undefined && csrfTokenIsNew) {
|
|
478
|
+
response.headers.append("set-cookie", serverActionCookie(csrfToken));
|
|
479
|
+
}
|
|
480
|
+
return response;
|
|
481
|
+
}
|
|
482
|
+
function modulePreloadTags(script, assetBaseUrl) {
|
|
483
|
+
return script === undefined
|
|
484
|
+
? ""
|
|
485
|
+
: `<link rel="modulepreload" href="${escapeHtmlAttribute(assetPath(script, assetBaseUrl ?? "/_mreact/client/"))}">`;
|
|
486
|
+
}
|
|
487
|
+
function isNavigationRequest(request) {
|
|
488
|
+
return request.headers.get("x-mreact-navigation") === "1";
|
|
489
|
+
}
|
|
490
|
+
async function nearestBoundaryFileForPage(options) {
|
|
491
|
+
const relativeDir = relative(options.appDir, dirname(options.pageFile));
|
|
492
|
+
const parts = relativeDir === "" ? [] : relativeDir.split(sep);
|
|
493
|
+
return nearestBoundaryFileFromParts({
|
|
494
|
+
appDir: options.appDir,
|
|
495
|
+
filename: options.filename,
|
|
496
|
+
parts,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async function nearestExistingBoundaryFileForPage(options) {
|
|
500
|
+
const relativeDir = relative(options.appDir, dirname(options.pageFile));
|
|
501
|
+
const parts = relativeDir === "" ? [] : relativeDir.split(sep);
|
|
502
|
+
return nearestExistingBoundaryFileFromParts({
|
|
503
|
+
appDir: options.appDir,
|
|
504
|
+
filename: options.filename,
|
|
505
|
+
parts,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async function nearestBoundaryFileForPath(options) {
|
|
509
|
+
const parts = options.pathname
|
|
510
|
+
.replace(/^\/+|\/+$/g, "")
|
|
511
|
+
.split("/")
|
|
512
|
+
.filter((part) => part.length > 0);
|
|
513
|
+
return nearestBoundaryFileFromParts({
|
|
514
|
+
appDir: options.appDir,
|
|
515
|
+
filename: options.filename,
|
|
516
|
+
parts,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
async function nearestBoundaryFileFromParts(options) {
|
|
520
|
+
for (let count = options.parts.length; count >= 0; count -= 1) {
|
|
521
|
+
for (const filename of boundaryFilenameCandidates(options.filename)) {
|
|
522
|
+
const candidate = join(options.appDir, ...options.parts.slice(0, count), filename);
|
|
523
|
+
try {
|
|
524
|
+
await access(candidate);
|
|
525
|
+
return candidate;
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// Keep walking toward the root boundary.
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return join(options.appDir, boundaryFilenameCandidates(options.filename)[0] ?? options.filename);
|
|
533
|
+
}
|
|
534
|
+
async function nearestExistingBoundaryFileFromParts(options) {
|
|
535
|
+
for (let count = options.parts.length; count >= 0; count -= 1) {
|
|
536
|
+
for (const filename of boundaryFilenameCandidates(options.filename)) {
|
|
537
|
+
const candidate = join(options.appDir, ...options.parts.slice(0, count), filename);
|
|
538
|
+
try {
|
|
539
|
+
await access(candidate);
|
|
540
|
+
return candidate;
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
// Keep walking toward the root boundary.
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
function boundaryFilenameCandidates(filename) {
|
|
550
|
+
if (!filename.endsWith(".mreact.tsx")) {
|
|
551
|
+
return [filename];
|
|
552
|
+
}
|
|
553
|
+
const standardFilename = filename.replace(".mreact.tsx", ".tsx");
|
|
554
|
+
return [standardFilename, filename];
|
|
555
|
+
}
|
|
556
|
+
async function renderSpecialRoute(options) {
|
|
557
|
+
try {
|
|
558
|
+
await access(options.routeFile);
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return new Response(options.textFallback, { status: options.status });
|
|
562
|
+
}
|
|
563
|
+
const props = {
|
|
564
|
+
data: undefined,
|
|
565
|
+
error: normalizeErrorForProps(options.error),
|
|
566
|
+
params: {},
|
|
567
|
+
queryClient: createQueryClient(),
|
|
568
|
+
request: options.request,
|
|
569
|
+
};
|
|
570
|
+
const pageHtml = await renderServerFileToHtml(options.routeFile, props, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
|
|
571
|
+
const pageHtmlForLayout = options.navigation?.clientRoute === true
|
|
572
|
+
? withHydrationMarkers({
|
|
573
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
574
|
+
clientReferenceManifest: undefined,
|
|
575
|
+
html: pageHtml,
|
|
576
|
+
props: options.navigation.props,
|
|
577
|
+
routePath: options.navigation.routePath,
|
|
578
|
+
script: options.navigation.script,
|
|
579
|
+
})
|
|
580
|
+
: pageHtml;
|
|
581
|
+
const html = await applyLayouts({
|
|
582
|
+
appDir: options.appDir,
|
|
583
|
+
pageFile: options.routeFile,
|
|
584
|
+
html: pageHtmlForLayout,
|
|
585
|
+
props,
|
|
586
|
+
serverModules: options.serverModules,
|
|
587
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
588
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
589
|
+
});
|
|
590
|
+
return new Response(`<!DOCTYPE html>${modulePreloadTags(options.navigation?.clientRoute === true ? options.navigation.script : undefined, options.assetBaseUrl)}${html}`, {
|
|
591
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
592
|
+
status: options.status,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
async function renderServerFileToHtml(file, props, serverModules, serverModuleCacheVersion, serverSourceFiles) {
|
|
596
|
+
const code = await readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles);
|
|
597
|
+
const output = transformServerModule({
|
|
598
|
+
code,
|
|
599
|
+
filename: file,
|
|
600
|
+
serverModules,
|
|
601
|
+
serverOutput: "string",
|
|
602
|
+
});
|
|
603
|
+
const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
|
|
604
|
+
if (fatalDiagnostics.length > 0) {
|
|
605
|
+
throw new Error(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"));
|
|
606
|
+
}
|
|
607
|
+
return runServerModule(output.code, props, file, serverModules, serverModuleCacheVersion);
|
|
608
|
+
}
|
|
609
|
+
function normalizeErrorForProps(error) {
|
|
610
|
+
if (error instanceof Error) {
|
|
611
|
+
return { message: error.message };
|
|
612
|
+
}
|
|
613
|
+
return { message: String(error) };
|
|
614
|
+
}
|
|
615
|
+
async function dispatchServerRoute(file, request) {
|
|
616
|
+
const module = await importAppRouterFileModule(file);
|
|
617
|
+
const handler = module[request.method] ?? module.ALL ?? module.default;
|
|
618
|
+
if (typeof handler !== "function") {
|
|
619
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
620
|
+
}
|
|
621
|
+
const response = await handler(request);
|
|
622
|
+
return response instanceof Response
|
|
623
|
+
? response
|
|
624
|
+
: new Response("Invalid route response", { status: 500 });
|
|
625
|
+
}
|
|
626
|
+
async function runMiddleware(options) {
|
|
627
|
+
const candidates = [
|
|
628
|
+
join(options.appDir, "middleware.ts"),
|
|
629
|
+
join(options.appDir, "middleware.mreact.ts"),
|
|
630
|
+
];
|
|
631
|
+
for (const file of candidates) {
|
|
632
|
+
try {
|
|
633
|
+
await access(file);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
const module = await loadMiddlewareModule({
|
|
639
|
+
appDir: options.appDir,
|
|
640
|
+
file,
|
|
641
|
+
importPolicy: options.importPolicy,
|
|
642
|
+
});
|
|
643
|
+
if (!middlewareMatches(module.config, new URL(options.request.url).pathname)) {
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
const middleware = module.middleware ?? module.default;
|
|
647
|
+
if (typeof middleware !== "function") {
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const response = await middleware(options.request);
|
|
652
|
+
return response instanceof Response ? response : undefined;
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
if (isRedirectError(error)) {
|
|
656
|
+
return new Response(null, {
|
|
657
|
+
headers: { location: error.location },
|
|
658
|
+
status: error.status,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (isNotFoundError(error)) {
|
|
662
|
+
return new Response("Not Found", { status: 404 });
|
|
663
|
+
}
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
async function loadMiddlewareModule(options) {
|
|
670
|
+
const code = await readFile(options.file, "utf8");
|
|
671
|
+
const output = await bundle({
|
|
672
|
+
bundle: true,
|
|
673
|
+
format: "esm",
|
|
674
|
+
logLevel: "silent",
|
|
675
|
+
platform: "node",
|
|
676
|
+
plugins: [
|
|
677
|
+
createAppRouterImportPolicyPlugin({
|
|
678
|
+
appDir: options.appDir,
|
|
679
|
+
importPolicy: options.importPolicy,
|
|
680
|
+
label: "Middleware",
|
|
681
|
+
}),
|
|
682
|
+
],
|
|
683
|
+
write: false,
|
|
684
|
+
jsx: "transform",
|
|
685
|
+
jsxFactory: "__mreact_jsx",
|
|
686
|
+
jsxFragment: "__mreact_fragment",
|
|
687
|
+
stdin: {
|
|
688
|
+
contents: code,
|
|
689
|
+
loader: "ts",
|
|
690
|
+
resolveDir: dirname(options.file),
|
|
691
|
+
sourcefile: options.file,
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
const compiled = output.outputFiles[0]?.text;
|
|
695
|
+
if (compiled === undefined) {
|
|
696
|
+
throw new Error(`Failed to compile middleware for ${options.file}.`);
|
|
697
|
+
}
|
|
698
|
+
return importAppRouterSourceModule({
|
|
699
|
+
code: compiled,
|
|
700
|
+
label: `middleware:${options.file}`,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function middlewareMatches(config, pathname) {
|
|
704
|
+
const matcher = config?.matcher;
|
|
705
|
+
if (matcher === undefined) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
if (matcher instanceof RegExp) {
|
|
709
|
+
return matcher.test(pathname);
|
|
710
|
+
}
|
|
711
|
+
if (Array.isArray(matcher)) {
|
|
712
|
+
return matcher.some((item) => middlewarePatternMatches(item, pathname));
|
|
713
|
+
}
|
|
714
|
+
return typeof matcher === "string" && middlewarePatternMatches(matcher, pathname);
|
|
715
|
+
}
|
|
716
|
+
function middlewarePatternMatches(pattern, pathname) {
|
|
717
|
+
if (pattern === pathname) {
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
if (pattern.endsWith("/:path*")) {
|
|
721
|
+
const prefix = pattern.slice(0, -"/:path*".length);
|
|
722
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
723
|
+
}
|
|
724
|
+
if (pattern.endsWith("*")) {
|
|
725
|
+
const prefix = pattern.slice(0, -1);
|
|
726
|
+
return pathname.startsWith(prefix);
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
function transformServerModule(options) {
|
|
731
|
+
const sourceHash = memoizedHashText(options.code);
|
|
732
|
+
const artifact = options.serverModules?.get(options.filename)?.[options.serverOutput];
|
|
733
|
+
if (artifact !== undefined &&
|
|
734
|
+
artifact.sourceHash === sourceHash &&
|
|
735
|
+
options.serverAwaitHydration !== true) {
|
|
736
|
+
return {
|
|
737
|
+
code: artifact.code,
|
|
738
|
+
diagnostics: [],
|
|
739
|
+
map: null,
|
|
740
|
+
metadata: artifact.metadata ?? {
|
|
741
|
+
compiler: {
|
|
742
|
+
frontend: "oxc",
|
|
743
|
+
typescriptFallback: false,
|
|
744
|
+
},
|
|
745
|
+
components: [],
|
|
746
|
+
filename: options.filename,
|
|
747
|
+
imports: [],
|
|
748
|
+
serverOutput: options.serverOutput,
|
|
749
|
+
target: "server",
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
const awaitHydrationKey = options.serverAwaitHydration === true ? "1" : "0";
|
|
754
|
+
const key = `${options.filename}\0${options.serverOutput}\0${sourceHash}\0${awaitHydrationKey}`;
|
|
755
|
+
const cached = serverTransformCache.get(key);
|
|
756
|
+
if (cached !== undefined) {
|
|
757
|
+
return cached;
|
|
758
|
+
}
|
|
759
|
+
const output = transform({
|
|
760
|
+
code: options.code,
|
|
761
|
+
...(options.clientBoundaryImports === undefined
|
|
762
|
+
? {}
|
|
763
|
+
: { clientBoundaryImports: options.clientBoundaryImports }),
|
|
764
|
+
dev: true,
|
|
765
|
+
filename: options.filename,
|
|
766
|
+
serverEscape: nativeEscapeTransform,
|
|
767
|
+
serverOutput: options.serverOutput,
|
|
768
|
+
target: "server",
|
|
769
|
+
...(options.serverAwaitHydration === true ? { serverAwaitHydration: true } : {}),
|
|
770
|
+
});
|
|
771
|
+
setBoundedCacheEntry(serverTransformCache, key, output, maxServerTransformCacheEntries);
|
|
772
|
+
return output;
|
|
773
|
+
}
|
|
774
|
+
async function analyzeRouteSource(options) {
|
|
775
|
+
const sourceHash = memoizedHashText(options.code);
|
|
776
|
+
const cacheKey = `${options.serverModuleCacheVersion ?? "dev"}\0${options.filename}\0${sourceHash}`;
|
|
777
|
+
const cached = routeSourceAnalysisCache.get(cacheKey);
|
|
778
|
+
if (cached !== undefined) {
|
|
779
|
+
return cached;
|
|
780
|
+
}
|
|
781
|
+
const pending = analyzeRouteSourceUncached(options).catch((error) => {
|
|
782
|
+
routeSourceAnalysisCache.delete(cacheKey);
|
|
783
|
+
throw error;
|
|
784
|
+
});
|
|
785
|
+
setBoundedCacheEntry(routeSourceAnalysisCache, cacheKey, pending, maxRouteSourceAnalysisCacheEntries);
|
|
786
|
+
return pending;
|
|
787
|
+
}
|
|
788
|
+
async function analyzeRouteSourceUncached(options) {
|
|
789
|
+
const routeCode = stripRouteModuleExports(options.code);
|
|
790
|
+
const clientInference = await inferClientRouteModule({
|
|
791
|
+
code: routeCode,
|
|
792
|
+
filename: options.filename,
|
|
793
|
+
routePath: options.routePath,
|
|
794
|
+
});
|
|
795
|
+
return {
|
|
796
|
+
authIncludesClaims: authIncludesClaims(options.code),
|
|
797
|
+
cachePolicy: routeCachePolicyFromSource(options.code),
|
|
798
|
+
clientInference,
|
|
799
|
+
hasLoader: hasLoaderExport(options.code),
|
|
800
|
+
routeCode,
|
|
801
|
+
streamRoute: isStreamRouteSource(options.code),
|
|
802
|
+
usesRuntimeCacheControl: usesRuntimeCacheControl(options.code),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
// Per-request hashText (SHA-256) is one of the hot path's dominant
|
|
806
|
+
// costs. Cache hashes for `code` strings we have already seen this
|
|
807
|
+
// process (common case: the prepared code is identical across requests
|
|
808
|
+
// when the source file is unchanged).
|
|
809
|
+
const codeHashCache = new Map();
|
|
810
|
+
const MAX_CODE_HASH_ENTRIES = 256;
|
|
811
|
+
function memoizedHashText(code) {
|
|
812
|
+
const cached = codeHashCache.get(code);
|
|
813
|
+
if (cached !== undefined) {
|
|
814
|
+
return cached;
|
|
815
|
+
}
|
|
816
|
+
const hash = hashText(code);
|
|
817
|
+
if (codeHashCache.size >= MAX_CODE_HASH_ENTRIES) {
|
|
818
|
+
// Simple LRU eviction: drop the oldest entry (Map keeps insertion order).
|
|
819
|
+
const oldestKey = codeHashCache.keys().next().value;
|
|
820
|
+
if (oldestKey !== undefined) {
|
|
821
|
+
codeHashCache.delete(oldestKey);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
codeHashCache.set(code, hash);
|
|
825
|
+
return hash;
|
|
826
|
+
}
|
|
827
|
+
async function runServerModule(code, props, sourcefile, serverModules, serverModuleCacheVersion) {
|
|
828
|
+
const component = await loadServerComponent(code, sourcefile, serverModules, serverModuleCacheVersion);
|
|
829
|
+
return component(props);
|
|
830
|
+
}
|
|
831
|
+
async function runServerModuleWithSlots(code, props, sourcefile, serverModules, serverModuleCacheVersion) {
|
|
832
|
+
const module = await loadServerModule(code, sourcefile, serverModules, serverModuleCacheVersion);
|
|
833
|
+
const component = selectServerComponent(module);
|
|
834
|
+
return {
|
|
835
|
+
html: await component(props),
|
|
836
|
+
slots: await renderRouteSlots(module.slots, props),
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
async function loadServerModule(code, sourcefile, serverModules, serverModuleCacheVersion) {
|
|
840
|
+
const artifact = serverModules?.get(sourcefile)?.string;
|
|
841
|
+
const codeHash = memoizedHashText(code);
|
|
842
|
+
const moduleCode = artifact !== undefined && artifact.sourceHash === codeHash ? artifact.code : code;
|
|
843
|
+
const cacheKey = serverModuleCacheVersion === undefined
|
|
844
|
+
? undefined
|
|
845
|
+
: `server-component:${serverModuleCacheVersion}:${sourcefile}:${moduleCode === code ? codeHash : memoizedHashText(moduleCode)}`;
|
|
846
|
+
return await importAppRouterSourceModule({
|
|
847
|
+
cacheKey,
|
|
848
|
+
code: moduleCode,
|
|
849
|
+
label: `server-component:${sourcefile}`,
|
|
850
|
+
resolveDir: dirname(sourcefile),
|
|
851
|
+
sourcefile,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
async function loadServerComponent(code, sourcefile, serverModules, serverModuleCacheVersion) {
|
|
855
|
+
const module = await loadServerModule(code, sourcefile, serverModules, serverModuleCacheVersion);
|
|
856
|
+
return selectServerComponent(module);
|
|
857
|
+
}
|
|
858
|
+
function selectServerComponent(module) {
|
|
859
|
+
const component = module.default ?? module.App ?? Object.values(module)[0];
|
|
860
|
+
if (typeof component !== "function") {
|
|
861
|
+
throw new Error("No page component export was found.");
|
|
862
|
+
}
|
|
863
|
+
return component;
|
|
864
|
+
}
|
|
865
|
+
async function renderRouteSlots(slots, props) {
|
|
866
|
+
if (slots === undefined) {
|
|
867
|
+
return {};
|
|
868
|
+
}
|
|
869
|
+
const rendered = {};
|
|
870
|
+
for (const [name, value] of Object.entries(slots)) {
|
|
871
|
+
rendered[name] = typeof value === "function" ? await value(props) : value;
|
|
872
|
+
}
|
|
873
|
+
return rendered;
|
|
874
|
+
}
|
|
875
|
+
function runServerStreamModule(code, options) {
|
|
876
|
+
return renderToReadableStream(async (sink) => {
|
|
877
|
+
const slots = await renderServerStreamSlots(code, {
|
|
878
|
+
pageFile: options.pageFile,
|
|
879
|
+
props: options.props,
|
|
880
|
+
serverModules: options.serverModules,
|
|
881
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
882
|
+
});
|
|
883
|
+
const layoutShells = await layoutShellsForPage(options.appDir, options.pageFile, options.props, slots, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
|
|
884
|
+
const marker = options.clientRoute
|
|
885
|
+
? hydrationMarkerParts({
|
|
886
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
887
|
+
clientReferenceManifest: options.clientReferenceManifest,
|
|
888
|
+
routePath: options.routePath,
|
|
889
|
+
script: options.script,
|
|
890
|
+
props: {
|
|
891
|
+
params: options.props.params,
|
|
892
|
+
request: { url: options.props.request.url },
|
|
893
|
+
data: options.props.data,
|
|
894
|
+
},
|
|
895
|
+
})
|
|
896
|
+
: undefined;
|
|
897
|
+
sink.append("<!DOCTYPE html>");
|
|
898
|
+
sink.append(modulePreloadTags(options.clientRoute ? options.script : undefined, options.assetBaseUrl));
|
|
899
|
+
for (const shell of layoutShells) {
|
|
900
|
+
sink.append(shell.prefix);
|
|
901
|
+
}
|
|
902
|
+
sink.append(marker?.prefix ?? "");
|
|
903
|
+
await appendServerStreamModule(code, sink, options.props, options.pageFile, options.serverModules, options.serverModuleCacheVersion);
|
|
904
|
+
sink.append(marker?.suffix ?? "");
|
|
905
|
+
for (const shell of [...layoutShells].reverse()) {
|
|
906
|
+
sink.append(shell.suffix);
|
|
907
|
+
}
|
|
908
|
+
if (hasOutOfOrderBoundary(code)) {
|
|
909
|
+
renderOutOfOrderReorderScript(sink);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
function hasOutOfOrderBoundary(code) {
|
|
914
|
+
return code.includes("renderOutOfOrderBoundary");
|
|
915
|
+
}
|
|
916
|
+
function mayRenderOutOfOrderBoundary(code) {
|
|
917
|
+
return (code.includes("<Await") || code.includes("Await(") || code.includes("renderOutOfOrderBoundary"));
|
|
918
|
+
}
|
|
919
|
+
async function runServerStreamModuleWithLoading(code, options) {
|
|
920
|
+
const loadingProps = {
|
|
921
|
+
data: undefined,
|
|
922
|
+
params: options.params,
|
|
923
|
+
queryClient: options.queryClient,
|
|
924
|
+
request: options.request,
|
|
925
|
+
};
|
|
926
|
+
const layoutShells = await layoutShellsForPage(options.appDir, options.pageFile, loadingProps, {}, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
|
|
927
|
+
const loadingHtml = await renderServerFileToHtml(options.loadingFile, loadingProps, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
|
|
928
|
+
const marker = options.clientRoute
|
|
929
|
+
? hydrationMarkerParts({
|
|
930
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
931
|
+
clientReferenceManifest: options.clientReferenceManifest,
|
|
932
|
+
routePath: options.routePath,
|
|
933
|
+
script: options.script,
|
|
934
|
+
props: {
|
|
935
|
+
params: options.params,
|
|
936
|
+
request: { url: options.request.url },
|
|
937
|
+
},
|
|
938
|
+
})
|
|
939
|
+
: undefined;
|
|
940
|
+
return renderToReadableStream((sink) => {
|
|
941
|
+
sink.append("<!DOCTYPE html>");
|
|
942
|
+
sink.append(modulePreloadTags(options.clientRoute ? options.script : undefined, options.assetBaseUrl));
|
|
943
|
+
for (const shell of layoutShells) {
|
|
944
|
+
sink.append(shell.prefix);
|
|
945
|
+
}
|
|
946
|
+
sink.append(marker?.prefix ?? "");
|
|
947
|
+
renderVisibleOutOfOrderBoundary(sink, "mreact-route", options.data, async (boundarySink, data) => {
|
|
948
|
+
await appendServerStreamModule(code, boundarySink, {
|
|
949
|
+
data,
|
|
950
|
+
params: options.params,
|
|
951
|
+
queryClient: options.queryClient,
|
|
952
|
+
request: options.request,
|
|
953
|
+
}, options.pageFile, options.serverModules, options.serverModuleCacheVersion);
|
|
954
|
+
}, {
|
|
955
|
+
placeholder(boundarySink) {
|
|
956
|
+
boundarySink.append(loadingHtml);
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
sink.append(marker?.suffix ?? "");
|
|
960
|
+
for (const shell of [...layoutShells].reverse()) {
|
|
961
|
+
sink.append(shell.suffix);
|
|
962
|
+
}
|
|
963
|
+
renderOutOfOrderReorderScript(sink);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
function renderVisibleOutOfOrderBoundary(sink, id, value, render, options = {}) {
|
|
967
|
+
const placeholderSink = createStringSink();
|
|
968
|
+
void options.placeholder?.(placeholderSink);
|
|
969
|
+
sink.append(`<span data-mreact-oob-placeholder="${escapeHtmlAttribute(id)}">${placeholderSink.toString()}</span>`);
|
|
970
|
+
const task = renderVisibleOutOfOrderFragment(sink, id, value, render, options);
|
|
971
|
+
if (sink.defer === undefined) {
|
|
972
|
+
void task;
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
sink.defer(task);
|
|
976
|
+
}
|
|
977
|
+
async function renderVisibleOutOfOrderFragment(sink, id, value, render, options) {
|
|
978
|
+
const fragmentSink = createStringSink();
|
|
979
|
+
await renderAsyncBoundary(fragmentSink, value, render, options.catch === undefined ? {} : { catch: options.catch });
|
|
980
|
+
sink.append(`<template data-mreact-oob-fragment="${escapeHtmlAttribute(id)}">${fragmentSink.toString()}</template>`);
|
|
981
|
+
}
|
|
982
|
+
async function appendServerStreamModule(code, sink, props, sourcefile, serverModules, serverModuleCacheVersion) {
|
|
983
|
+
const module = await loadServerStreamModule(code, sourcefile, serverModules, serverModuleCacheVersion);
|
|
984
|
+
const component = selectStreamComponent(module);
|
|
985
|
+
await component(sink, props);
|
|
986
|
+
}
|
|
987
|
+
async function renderServerStreamSlots(code, options) {
|
|
988
|
+
if (!hasRouteSlotsExport(code)) {
|
|
989
|
+
return {};
|
|
990
|
+
}
|
|
991
|
+
const module = await loadServerStreamModule(code, options.pageFile, options.serverModules, options.serverModuleCacheVersion);
|
|
992
|
+
if (module.slots === undefined) {
|
|
993
|
+
return {};
|
|
994
|
+
}
|
|
995
|
+
const rendered = {};
|
|
996
|
+
for (const [name, value] of Object.entries(module.slots)) {
|
|
997
|
+
if (typeof value !== "function") {
|
|
998
|
+
rendered[name] = value;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const sink = createStringSink();
|
|
1002
|
+
await value(sink, options.props);
|
|
1003
|
+
await sink.drain();
|
|
1004
|
+
rendered[name] = sink.toString();
|
|
1005
|
+
}
|
|
1006
|
+
return rendered;
|
|
1007
|
+
}
|
|
1008
|
+
function hasRouteSlotsExport(code) {
|
|
1009
|
+
return /^\s*export\s+const\s+slots\s*=/m.test(code);
|
|
1010
|
+
}
|
|
1011
|
+
async function loadServerStreamModule(code, sourcefile, serverModules, serverModuleCacheVersion) {
|
|
1012
|
+
const artifactCode = serverModules?.get(sourcefile)?.stream;
|
|
1013
|
+
const codeHash = memoizedHashText(code);
|
|
1014
|
+
const moduleCode = artifactCode !== undefined && artifactCode.sourceHash === codeHash ? artifactCode.code : code;
|
|
1015
|
+
const cacheKey = serverModuleCacheVersion === undefined
|
|
1016
|
+
? undefined
|
|
1017
|
+
: `server-stream-component:${serverModuleCacheVersion}:${sourcefile}:${moduleCode === code ? codeHash : memoizedHashText(moduleCode)}`;
|
|
1018
|
+
return await importAppRouterSourceModule({
|
|
1019
|
+
cacheKey,
|
|
1020
|
+
code: moduleCode,
|
|
1021
|
+
label: `server-stream-component:${sourcefile}`,
|
|
1022
|
+
resolveDir: dirname(sourcefile),
|
|
1023
|
+
sourcefile,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
function selectStreamComponent(module) {
|
|
1027
|
+
const component = module.default ?? module.App ?? Object.values(module)[0];
|
|
1028
|
+
if (typeof component !== "function") {
|
|
1029
|
+
throw new Error("No page component export was found.");
|
|
1030
|
+
}
|
|
1031
|
+
return component;
|
|
1032
|
+
}
|
|
1033
|
+
async function applyLayouts(options) {
|
|
1034
|
+
const layoutFiles = await shellFilesForPage(options.appDir, options.pageFile, options.serverModuleCacheVersion);
|
|
1035
|
+
let html = options.html;
|
|
1036
|
+
const slotContext = createSlotRenderContext(options.slots);
|
|
1037
|
+
for (const shell of layoutFiles.reverse()) {
|
|
1038
|
+
const rendered = await renderShellPrefixSuffix(options.appDir, shell, options.props, slotContext, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
|
|
1039
|
+
html = `${rendered.prefix}${html}${rendered.suffix}`;
|
|
1040
|
+
}
|
|
1041
|
+
warnUnconsumedRouteSlots({
|
|
1042
|
+
appDir: options.appDir,
|
|
1043
|
+
pageFile: options.pageFile,
|
|
1044
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
1045
|
+
slotContext,
|
|
1046
|
+
});
|
|
1047
|
+
return html;
|
|
1048
|
+
}
|
|
1049
|
+
async function layoutShellsForPage(appDir, pageFile, props, slots, serverModules, serverModuleCacheVersion, serverSourceFiles) {
|
|
1050
|
+
const layoutFiles = await shellFilesForPage(appDir, pageFile, serverModuleCacheVersion);
|
|
1051
|
+
const shells = [];
|
|
1052
|
+
const slotContext = createSlotRenderContext(slots);
|
|
1053
|
+
for (const shell of layoutFiles) {
|
|
1054
|
+
shells.push(await renderShellPrefixSuffix(appDir, shell, props, slotContext, serverModules, serverModuleCacheVersion, serverSourceFiles));
|
|
1055
|
+
}
|
|
1056
|
+
warnUnconsumedRouteSlots({
|
|
1057
|
+
appDir,
|
|
1058
|
+
pageFile,
|
|
1059
|
+
serverModuleCacheVersion,
|
|
1060
|
+
slotContext,
|
|
1061
|
+
});
|
|
1062
|
+
return shells;
|
|
1063
|
+
}
|
|
1064
|
+
async function renderShellPrefixSuffix(appDir, shell, props, slotContext, serverModules, serverModuleCacheVersion, serverSourceFiles) {
|
|
1065
|
+
const hasNamedSlots = Object.keys(slotContext.namedSlots).length > 0;
|
|
1066
|
+
const cacheKey = serverModuleCacheVersion === undefined || hasNamedSlots || shell.kind === "template"
|
|
1067
|
+
? undefined
|
|
1068
|
+
: `${appDir}\0${shell.file}\0${serverModuleCacheVersion}`;
|
|
1069
|
+
if (cacheKey !== undefined) {
|
|
1070
|
+
const cached = renderedShellCache.get(cacheKey);
|
|
1071
|
+
if (cached !== undefined && cached !== "impure") {
|
|
1072
|
+
return cached;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
const code = await readServerSourceFile(shell.file, serverModuleCacheVersion, serverSourceFiles);
|
|
1076
|
+
const output = transformServerModule({
|
|
1077
|
+
code,
|
|
1078
|
+
filename: shell.file,
|
|
1079
|
+
serverModules,
|
|
1080
|
+
serverOutput: "string",
|
|
1081
|
+
});
|
|
1082
|
+
const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
|
|
1083
|
+
if (fatalDiagnostics.length > 0) {
|
|
1084
|
+
throw new Error(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"));
|
|
1085
|
+
}
|
|
1086
|
+
const component = await loadServerComponent(output.code, shell.file, serverModules, serverModuleCacheVersion);
|
|
1087
|
+
const rendered = splitLayoutSlot(markShellBoundary(await component(props), shell), slotContext);
|
|
1088
|
+
const cached = cacheKey !== undefined ? renderedShellCache.get(cacheKey) : undefined;
|
|
1089
|
+
// Detect purity: a zero-arg component cannot depend on props. The
|
|
1090
|
+
// markShellBoundary + splitLayoutSlot output is then constant for
|
|
1091
|
+
// the (appDir, shellFile, version) tuple. We only set the cache
|
|
1092
|
+
// entry on the first request that observes the function arity; on
|
|
1093
|
+
// an "impure" tag we never overwrite it.
|
|
1094
|
+
if (cacheKey !== undefined && cached !== "impure") {
|
|
1095
|
+
if (component.length === 0) {
|
|
1096
|
+
if (renderedShellCache.size >= MAX_RENDERED_SHELL_CACHE_ENTRIES) {
|
|
1097
|
+
const oldestKey = renderedShellCache.keys().next().value;
|
|
1098
|
+
if (oldestKey !== undefined) {
|
|
1099
|
+
renderedShellCache.delete(oldestKey);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
renderedShellCache.set(cacheKey, rendered);
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
// Impure — stamp the cache so subsequent lookups short-circuit
|
|
1106
|
+
// without re-checking arity. We still run the per-request
|
|
1107
|
+
// render path above so the props are honoured.
|
|
1108
|
+
renderedShellCache.set(cacheKey, "impure");
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return rendered;
|
|
1112
|
+
}
|
|
1113
|
+
function splitLayoutSlot(layoutHtml, slotContext = createSlotRenderContext()) {
|
|
1114
|
+
const html = replaceNamedLayoutSlots(layoutHtml, slotContext);
|
|
1115
|
+
const match = findDefaultLayoutSlot(html);
|
|
1116
|
+
if (match === null) {
|
|
1117
|
+
return { prefix: html, suffix: "" };
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
prefix: html.slice(0, match.index),
|
|
1121
|
+
suffix: html.slice(match.index + match[0].length),
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
// Layout/template files for a given page do not change during a server's
|
|
1125
|
+
// lifetime in production. Each cache miss costs up to N×4 filesystem
|
|
1126
|
+
// `access()` syscalls (~5-10μs each on a fast SSD), making this one of
|
|
1127
|
+
// the largest fixed costs in `renderBuiltAppRequest` for a minimal page.
|
|
1128
|
+
//
|
|
1129
|
+
// We cache by `appDir + pageFile + serverModuleCacheVersion` so the cache
|
|
1130
|
+
// is only active when a server-module manifest version is available
|
|
1131
|
+
// (= production builds). In dev mode the version is `undefined`, so we
|
|
1132
|
+
// skip the cache and pick up newly added layout / template files on the
|
|
1133
|
+
// next request.
|
|
1134
|
+
const shellFilesCache = new Map();
|
|
1135
|
+
const MAX_SHELL_FILES_CACHE_ENTRIES = 1024;
|
|
1136
|
+
async function shellFilesForPage(appDir, pageFile, serverModuleCacheVersion) {
|
|
1137
|
+
const cacheKey = serverModuleCacheVersion === undefined
|
|
1138
|
+
? undefined
|
|
1139
|
+
: `${appDir}\0${pageFile}\0${serverModuleCacheVersion}`;
|
|
1140
|
+
if (cacheKey !== undefined) {
|
|
1141
|
+
const cached = shellFilesCache.get(cacheKey);
|
|
1142
|
+
if (cached !== undefined) {
|
|
1143
|
+
return cached;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const relativeDir = relative(appDir, dirname(pageFile));
|
|
1147
|
+
const parts = relativeDir === "" ? [] : relativeDir.split("/");
|
|
1148
|
+
const directories = [appDir];
|
|
1149
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
1150
|
+
directories.push(join(appDir, ...parts.slice(0, index + 1)));
|
|
1151
|
+
}
|
|
1152
|
+
const files = [];
|
|
1153
|
+
for (const directory of directories) {
|
|
1154
|
+
const shellId = shellBoundaryId(appDir, directory);
|
|
1155
|
+
for (const [filename, kind] of [
|
|
1156
|
+
["layout.tsx", "layout"],
|
|
1157
|
+
["layout.mreact.tsx", "layout"],
|
|
1158
|
+
["template.tsx", "template"],
|
|
1159
|
+
["template.mreact.tsx", "template"],
|
|
1160
|
+
]) {
|
|
1161
|
+
const candidate = join(directory, filename);
|
|
1162
|
+
try {
|
|
1163
|
+
await access(candidate);
|
|
1164
|
+
files.push({ file: candidate, id: shellId, kind });
|
|
1165
|
+
}
|
|
1166
|
+
catch {
|
|
1167
|
+
// Missing shell files are allowed.
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (cacheKey !== undefined) {
|
|
1172
|
+
if (shellFilesCache.size >= MAX_SHELL_FILES_CACHE_ENTRIES) {
|
|
1173
|
+
const oldestKey = shellFilesCache.keys().next().value;
|
|
1174
|
+
if (oldestKey !== undefined) {
|
|
1175
|
+
shellFilesCache.delete(oldestKey);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
shellFilesCache.set(cacheKey, files);
|
|
1179
|
+
}
|
|
1180
|
+
return files;
|
|
1181
|
+
}
|
|
1182
|
+
function withRouteCacheHeader(response, policy) {
|
|
1183
|
+
if (policy !== undefined) {
|
|
1184
|
+
response.headers.set("cache-control", policy.cacheControl);
|
|
1185
|
+
}
|
|
1186
|
+
return response;
|
|
1187
|
+
}
|
|
1188
|
+
function shellBoundaryId(appDir, directory) {
|
|
1189
|
+
const relativeDirectory = relative(appDir, directory);
|
|
1190
|
+
return relativeDirectory === ""
|
|
1191
|
+
? "root"
|
|
1192
|
+
: relativeDirectory.replaceAll(sep, "/").replace(/[^A-Za-z0-9_$/-]/g, "_");
|
|
1193
|
+
}
|
|
1194
|
+
function markShellBoundary(html, shell) {
|
|
1195
|
+
const attributeName = shell.kind === "layout" ? "data-mreact-layout-boundary" : "data-mreact-template-boundary";
|
|
1196
|
+
if (html.includes(`${attributeName}=`)) {
|
|
1197
|
+
return html;
|
|
1198
|
+
}
|
|
1199
|
+
return html.replace(/<([A-Za-z][^\s/>]*)([^>]*)>/, `<$1$2 ${attributeName}="${escapeHtmlAttribute(shell.id)}">`);
|
|
1200
|
+
}
|
|
1201
|
+
function replaceLayoutSlot(layoutHtml, childHtml, slotContext = createSlotRenderContext()) {
|
|
1202
|
+
const html = replaceNamedLayoutSlots(layoutHtml, slotContext);
|
|
1203
|
+
const match = findDefaultLayoutSlot(html);
|
|
1204
|
+
return match === null
|
|
1205
|
+
? `${html}${childHtml}`
|
|
1206
|
+
: `${html.slice(0, match.index)}${childHtml}${html.slice(match.index + match[0].length)}`;
|
|
1207
|
+
}
|
|
1208
|
+
function replaceNamedLayoutSlots(layoutHtml, slotContext) {
|
|
1209
|
+
return layoutHtml.replace(SLOT_TAG_PATTERN, (source, openAttributes) => {
|
|
1210
|
+
const name = readSlotName(openAttributes);
|
|
1211
|
+
if (name === undefined || name === "default") {
|
|
1212
|
+
return source;
|
|
1213
|
+
}
|
|
1214
|
+
if (Object.hasOwn(slotContext.namedSlots, name)) {
|
|
1215
|
+
slotContext.consumedSlots.add(name);
|
|
1216
|
+
return slotContext.namedSlots[name] ?? "";
|
|
1217
|
+
}
|
|
1218
|
+
return "";
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
const SLOT_TAG_PATTERN = /<slot\b([^>]*)>(?:<\/slot\s*>)?/g;
|
|
1222
|
+
function findDefaultLayoutSlot(html) {
|
|
1223
|
+
SLOT_TAG_PATTERN.lastIndex = 0;
|
|
1224
|
+
for (;;) {
|
|
1225
|
+
const match = SLOT_TAG_PATTERN.exec(html);
|
|
1226
|
+
if (match === null) {
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
const name = readSlotName(match[1] ?? "");
|
|
1230
|
+
if (name === undefined || name === "default") {
|
|
1231
|
+
return match;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function readSlotName(attributes) {
|
|
1236
|
+
const match = /\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(attributes);
|
|
1237
|
+
return match?.[1] ?? match?.[2];
|
|
1238
|
+
}
|
|
1239
|
+
function createSlotRenderContext(namedSlots = {}) {
|
|
1240
|
+
return {
|
|
1241
|
+
consumedSlots: new Set(),
|
|
1242
|
+
namedSlots,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function warnUnconsumedRouteSlots(options) {
|
|
1246
|
+
if (options.serverModuleCacheVersion !== undefined) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const slotNames = Object.keys(options.slotContext.namedSlots);
|
|
1250
|
+
if (slotNames.length === 0) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const routeLabel = relative(options.appDir, options.pageFile).replaceAll(sep, "/");
|
|
1254
|
+
for (const name of slotNames) {
|
|
1255
|
+
if (name === "default") {
|
|
1256
|
+
console.warn(`[mreact] ${routeLabel}: slots.default does not target <Slot />; use the page body for default slot content.`);
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (!options.slotContext.consumedSlots.has(name)) {
|
|
1260
|
+
console.warn(`[mreact] ${routeLabel}: slots.{${name}} is not consumed by any ancestor layout or template.`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
async function loadRouteData(options) {
|
|
1265
|
+
if (!hasLoaderExport(options.code)) {
|
|
1266
|
+
return undefined;
|
|
1267
|
+
}
|
|
1268
|
+
const output = await bundle({
|
|
1269
|
+
bundle: true,
|
|
1270
|
+
format: "esm",
|
|
1271
|
+
logLevel: "silent",
|
|
1272
|
+
platform: "node",
|
|
1273
|
+
plugins: [
|
|
1274
|
+
createAppRouterImportPolicyPlugin({
|
|
1275
|
+
appDir: options.appDir,
|
|
1276
|
+
importPolicy: options.importPolicy,
|
|
1277
|
+
label: "Loader",
|
|
1278
|
+
}),
|
|
1279
|
+
],
|
|
1280
|
+
write: false,
|
|
1281
|
+
jsx: "transform",
|
|
1282
|
+
jsxFactory: "__mreact_jsx",
|
|
1283
|
+
jsxFragment: "__mreact_fragment",
|
|
1284
|
+
stdin: {
|
|
1285
|
+
contents: options.code,
|
|
1286
|
+
loader: "tsx",
|
|
1287
|
+
resolveDir: dirname(options.filename),
|
|
1288
|
+
sourcefile: options.filename,
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
const code = output.outputFiles[0]?.text;
|
|
1292
|
+
if (code === undefined) {
|
|
1293
|
+
throw new Error(`Failed to compile loader for ${options.filename}.`);
|
|
1294
|
+
}
|
|
1295
|
+
const module = await importAppRouterSourceModule({
|
|
1296
|
+
code,
|
|
1297
|
+
label: `loader:${options.filename}`,
|
|
1298
|
+
});
|
|
1299
|
+
return module.loader === undefined ? undefined : await module.loader(options.context);
|
|
1300
|
+
}
|
|
1301
|
+
async function loadRouteMetadata(options) {
|
|
1302
|
+
if (!hasMetadataExport(options.code)) {
|
|
1303
|
+
return undefined;
|
|
1304
|
+
}
|
|
1305
|
+
const output = await bundle({
|
|
1306
|
+
bundle: true,
|
|
1307
|
+
format: "esm",
|
|
1308
|
+
logLevel: "silent",
|
|
1309
|
+
platform: "node",
|
|
1310
|
+
plugins: [
|
|
1311
|
+
createAppRouterImportPolicyPlugin({
|
|
1312
|
+
appDir: options.appDir,
|
|
1313
|
+
importPolicy: options.importPolicy,
|
|
1314
|
+
label: "Metadata",
|
|
1315
|
+
}),
|
|
1316
|
+
],
|
|
1317
|
+
write: false,
|
|
1318
|
+
jsx: "transform",
|
|
1319
|
+
jsxFactory: "__mreact_jsx",
|
|
1320
|
+
jsxFragment: "__mreact_fragment",
|
|
1321
|
+
stdin: {
|
|
1322
|
+
contents: options.code,
|
|
1323
|
+
loader: "tsx",
|
|
1324
|
+
resolveDir: dirname(options.filename),
|
|
1325
|
+
sourcefile: options.filename,
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1328
|
+
const code = output.outputFiles[0]?.text;
|
|
1329
|
+
if (code === undefined) {
|
|
1330
|
+
throw new Error(`Failed to compile metadata for ${options.filename}.`);
|
|
1331
|
+
}
|
|
1332
|
+
const module = await importAppRouterSourceModule({
|
|
1333
|
+
code,
|
|
1334
|
+
label: `metadata:${options.filename}`,
|
|
1335
|
+
});
|
|
1336
|
+
return module.metadata;
|
|
1337
|
+
}
|
|
1338
|
+
async function loadComposedRouteMetadata(options) {
|
|
1339
|
+
const cacheKey = options.serverModuleCacheVersion === undefined
|
|
1340
|
+
? undefined
|
|
1341
|
+
: `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}`;
|
|
1342
|
+
if (cacheKey !== undefined) {
|
|
1343
|
+
const cached = composedRouteMetadataCache.get(cacheKey);
|
|
1344
|
+
if (cached !== undefined) {
|
|
1345
|
+
return cached;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const loaded = loadComposedRouteMetadataUncached(options).catch((error) => {
|
|
1349
|
+
if (cacheKey !== undefined) {
|
|
1350
|
+
composedRouteMetadataCache.delete(cacheKey);
|
|
1351
|
+
}
|
|
1352
|
+
throw error;
|
|
1353
|
+
});
|
|
1354
|
+
if (cacheKey !== undefined) {
|
|
1355
|
+
setBoundedCacheEntry(composedRouteMetadataCache, cacheKey, loaded, maxComposedRouteMetadataCacheEntries);
|
|
1356
|
+
}
|
|
1357
|
+
return loaded;
|
|
1358
|
+
}
|
|
1359
|
+
async function loadComposedRouteMetadataUncached(options) {
|
|
1360
|
+
const layoutFiles = await shellFilesForPage(options.appDir, options.filename, options.serverModuleCacheVersion);
|
|
1361
|
+
const metadata = [];
|
|
1362
|
+
for (const shell of layoutFiles) {
|
|
1363
|
+
if (shell.kind !== "layout") {
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
const code = await readServerSourceFile(shell.file, options.serverModuleCacheVersion, options.serverSourceFiles);
|
|
1367
|
+
const shellMetadata = await loadRouteMetadata({
|
|
1368
|
+
appDir: options.appDir,
|
|
1369
|
+
code,
|
|
1370
|
+
filename: shell.file,
|
|
1371
|
+
importPolicy: options.importPolicy,
|
|
1372
|
+
});
|
|
1373
|
+
if (shellMetadata !== undefined) {
|
|
1374
|
+
metadata.push(shellMetadata);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const pageMetadata = await loadRouteMetadata({
|
|
1378
|
+
appDir: options.appDir,
|
|
1379
|
+
code: options.code,
|
|
1380
|
+
filename: options.filename,
|
|
1381
|
+
importPolicy: options.importPolicy,
|
|
1382
|
+
});
|
|
1383
|
+
if (pageMetadata !== undefined) {
|
|
1384
|
+
metadata.push(pageMetadata);
|
|
1385
|
+
}
|
|
1386
|
+
return mergeRouteMetadata(metadata);
|
|
1387
|
+
}
|
|
1388
|
+
function mergeRouteMetadata(metadata) {
|
|
1389
|
+
if (metadata.length === 0) {
|
|
1390
|
+
return undefined;
|
|
1391
|
+
}
|
|
1392
|
+
return metadata.reduce((merged, next) => {
|
|
1393
|
+
const mergedMetadata = { ...merged, ...next };
|
|
1394
|
+
const alternates = mergeObject(merged.alternates, next.alternates);
|
|
1395
|
+
const csp = mergeCspMetadata(merged.csp, next.csp);
|
|
1396
|
+
const head = mergeReadonlyArrays(merged.head, next.head);
|
|
1397
|
+
const icons = mergeObject(merged.icons, next.icons);
|
|
1398
|
+
const openGraph = mergeOpenGraphMetadata(merged.openGraph, next.openGraph);
|
|
1399
|
+
if (alternates !== undefined) {
|
|
1400
|
+
mergedMetadata.alternates = alternates;
|
|
1401
|
+
}
|
|
1402
|
+
if (csp !== undefined) {
|
|
1403
|
+
mergedMetadata.csp = csp;
|
|
1404
|
+
}
|
|
1405
|
+
if (head !== undefined) {
|
|
1406
|
+
mergedMetadata.head = head;
|
|
1407
|
+
}
|
|
1408
|
+
if (icons !== undefined) {
|
|
1409
|
+
mergedMetadata.icons = icons;
|
|
1410
|
+
}
|
|
1411
|
+
if (openGraph !== undefined) {
|
|
1412
|
+
mergedMetadata.openGraph = openGraph;
|
|
1413
|
+
}
|
|
1414
|
+
return mergedMetadata;
|
|
1415
|
+
}, {});
|
|
1416
|
+
}
|
|
1417
|
+
function mergeObject(left, right) {
|
|
1418
|
+
if (left === undefined) {
|
|
1419
|
+
return right;
|
|
1420
|
+
}
|
|
1421
|
+
if (right === undefined) {
|
|
1422
|
+
return left;
|
|
1423
|
+
}
|
|
1424
|
+
return { ...left, ...right };
|
|
1425
|
+
}
|
|
1426
|
+
function mergeReadonlyArrays(left, right) {
|
|
1427
|
+
if (left === undefined || left.length === 0) {
|
|
1428
|
+
return right;
|
|
1429
|
+
}
|
|
1430
|
+
if (right === undefined || right.length === 0) {
|
|
1431
|
+
return left;
|
|
1432
|
+
}
|
|
1433
|
+
return [...left, ...right];
|
|
1434
|
+
}
|
|
1435
|
+
function mergeCspMetadata(left, right) {
|
|
1436
|
+
if (left === undefined) {
|
|
1437
|
+
return right;
|
|
1438
|
+
}
|
|
1439
|
+
if (right === undefined) {
|
|
1440
|
+
return left;
|
|
1441
|
+
}
|
|
1442
|
+
const merged = {
|
|
1443
|
+
...left,
|
|
1444
|
+
...right,
|
|
1445
|
+
};
|
|
1446
|
+
const directives = mergeObject(left.directives, right.directives);
|
|
1447
|
+
if (directives !== undefined) {
|
|
1448
|
+
merged.directives = directives;
|
|
1449
|
+
}
|
|
1450
|
+
return merged;
|
|
1451
|
+
}
|
|
1452
|
+
function mergeOpenGraphMetadata(left, right) {
|
|
1453
|
+
if (left === undefined) {
|
|
1454
|
+
return right;
|
|
1455
|
+
}
|
|
1456
|
+
if (right === undefined) {
|
|
1457
|
+
return left;
|
|
1458
|
+
}
|
|
1459
|
+
const merged = {
|
|
1460
|
+
...left,
|
|
1461
|
+
...right,
|
|
1462
|
+
};
|
|
1463
|
+
const images = mergeReadonlyArrays(openGraphImages(left), openGraphImages(right));
|
|
1464
|
+
if (images !== undefined && images.length > 0) {
|
|
1465
|
+
merged.images = images;
|
|
1466
|
+
}
|
|
1467
|
+
return merged;
|
|
1468
|
+
}
|
|
1469
|
+
function hasMetadataExport(code) {
|
|
1470
|
+
return /\bexport\s+const\s+metadata\s*=/.test(code);
|
|
1471
|
+
}
|
|
1472
|
+
function usesRuntimeCacheControl(code) {
|
|
1473
|
+
return /\bcacheControl\s*\(/.test(code);
|
|
1474
|
+
}
|
|
1475
|
+
function injectHeadMetadata(html, metadata) {
|
|
1476
|
+
if (metadata === undefined) {
|
|
1477
|
+
return html;
|
|
1478
|
+
}
|
|
1479
|
+
const tags = [
|
|
1480
|
+
metadata.title === undefined
|
|
1481
|
+
? undefined
|
|
1482
|
+
: `<title>${escapeHtml(metadataString(metadata.title, "title"))}</title>`,
|
|
1483
|
+
metadata.description === undefined
|
|
1484
|
+
? undefined
|
|
1485
|
+
: `<meta name="description" content="${escapeHtmlAttribute(metadataString(metadata.description, "description"))}">`,
|
|
1486
|
+
metadata.alternates?.canonical === undefined
|
|
1487
|
+
? undefined
|
|
1488
|
+
: `<link rel="canonical" href="${escapeHtmlAttribute(metadataString(metadata.alternates.canonical, "alternates.canonical"))}">`,
|
|
1489
|
+
metadata.openGraph?.title === undefined
|
|
1490
|
+
? undefined
|
|
1491
|
+
: `<meta property="og:title" content="${escapeHtmlAttribute(metadataString(metadata.openGraph.title, "openGraph.title"))}">`,
|
|
1492
|
+
metadata.openGraph?.description === undefined
|
|
1493
|
+
? undefined
|
|
1494
|
+
: `<meta property="og:description" content="${escapeHtmlAttribute(metadataString(metadata.openGraph.description, "openGraph.description"))}">`,
|
|
1495
|
+
...openGraphImages(metadata.openGraph).map((image) => `<meta property="og:image" content="${escapeHtmlAttribute(image)}">`),
|
|
1496
|
+
metadata.icons?.icon === undefined
|
|
1497
|
+
? undefined
|
|
1498
|
+
: `<link rel="icon" href="${escapeHtmlAttribute(metadataString(metadata.icons.icon, "icons.icon"))}">`,
|
|
1499
|
+
metadata.icons?.apple === undefined
|
|
1500
|
+
? undefined
|
|
1501
|
+
: `<link rel="apple-touch-icon" href="${escapeHtmlAttribute(metadataString(metadata.icons.apple, "icons.apple"))}">`,
|
|
1502
|
+
metadata.robots === undefined
|
|
1503
|
+
? undefined
|
|
1504
|
+
: `<meta name="robots" content="${escapeHtmlAttribute(robotsContent(metadata.robots))}">`,
|
|
1505
|
+
metadata.themeColor === undefined ? undefined : themeColorTag(metadata.themeColor),
|
|
1506
|
+
metadata.viewport === undefined
|
|
1507
|
+
? undefined
|
|
1508
|
+
: `<meta name="viewport" content="${escapeHtmlAttribute(viewportContent(metadata.viewport))}">`,
|
|
1509
|
+
...headDescriptorTags(metadata.head, metadata.csp?.nonce),
|
|
1510
|
+
]
|
|
1511
|
+
.filter((tag) => tag !== undefined)
|
|
1512
|
+
.join("");
|
|
1513
|
+
if (tags === "") {
|
|
1514
|
+
return html;
|
|
1515
|
+
}
|
|
1516
|
+
if (/<head(?:\s[^>]*)?>/i.test(html)) {
|
|
1517
|
+
return html.replace(/<head(\s[^>]*)?>/i, (match) => `${match}${tags}`);
|
|
1518
|
+
}
|
|
1519
|
+
if (/<html(?:\s[^>]*)?>/i.test(html)) {
|
|
1520
|
+
return html.replace(/<html(\s[^>]*)?>/i, (match) => `${match}<head>${tags}</head>`);
|
|
1521
|
+
}
|
|
1522
|
+
return `<head>${tags}</head>${html}`;
|
|
1523
|
+
}
|
|
1524
|
+
function responseHeadersForMetadata(metadata) {
|
|
1525
|
+
const headers = new Headers({ "content-type": "text/html; charset=utf-8" });
|
|
1526
|
+
const csp = contentSecurityPolicy(metadata?.csp);
|
|
1527
|
+
if (csp !== undefined) {
|
|
1528
|
+
headers.set("content-security-policy", csp);
|
|
1529
|
+
}
|
|
1530
|
+
return headers;
|
|
1531
|
+
}
|
|
1532
|
+
function injectQueryState(html, state) {
|
|
1533
|
+
if (state.queries.length === 0) {
|
|
1534
|
+
return html;
|
|
1535
|
+
}
|
|
1536
|
+
const script = `<script type="application/json" id="${__MREACT_QUERY_STATE_SCRIPT_ID}">${escapeJsonForHtml(JSON.stringify(state))}</script>`;
|
|
1537
|
+
return /<\/body>/i.test(html)
|
|
1538
|
+
? html.replace(/<\/body>/i, `${script}</body>`)
|
|
1539
|
+
: `${html}${script}`;
|
|
1540
|
+
}
|
|
1541
|
+
function injectAuthSessionClaims(html, claims) {
|
|
1542
|
+
if (claims === undefined) {
|
|
1543
|
+
return html;
|
|
1544
|
+
}
|
|
1545
|
+
const script = `<script type="application/json" id="${authSessionScriptId}">${escapeJsonForHtml(JSON.stringify(claims))}</script>`;
|
|
1546
|
+
return /<\/body>/i.test(html)
|
|
1547
|
+
? html.replace(/<\/body>/i, `${script}</body>`)
|
|
1548
|
+
: `${html}${script}`;
|
|
1549
|
+
}
|
|
1550
|
+
function authIncludesClaims(code) {
|
|
1551
|
+
return /\bexport\s+const\s+auth\s*=\s*["']include-claims["']\s*;?/.test(code);
|
|
1552
|
+
}
|
|
1553
|
+
function currentAuthClaims() {
|
|
1554
|
+
return authRequestStorage().getStore()?.claims;
|
|
1555
|
+
}
|
|
1556
|
+
function authRequestStorage() {
|
|
1557
|
+
const global = globalThis;
|
|
1558
|
+
global[authRuntimeStateKey] ??= {};
|
|
1559
|
+
global[authRuntimeStateKey].storage ??= new AsyncLocalStorage();
|
|
1560
|
+
return global[authRuntimeStateKey].storage;
|
|
1561
|
+
}
|
|
1562
|
+
function escapeJsonForHtml(value) {
|
|
1563
|
+
return value
|
|
1564
|
+
.replaceAll("&", "\\u0026")
|
|
1565
|
+
.replaceAll("<", "\\u003c")
|
|
1566
|
+
.replaceAll(">", "\\u003e")
|
|
1567
|
+
.replaceAll("\u2028", "\\u2028")
|
|
1568
|
+
.replaceAll("\u2029", "\\u2029");
|
|
1569
|
+
}
|
|
1570
|
+
function headDescriptorTags(descriptors, nonce) {
|
|
1571
|
+
return (descriptors ?? []).flatMap((descriptor) => {
|
|
1572
|
+
const descriptorNonce = descriptor.nonce === true ? nonce : descriptor.nonce || undefined;
|
|
1573
|
+
const attrs = {
|
|
1574
|
+
...descriptor.attrs,
|
|
1575
|
+
...(descriptorNonce === undefined ? {} : { nonce: descriptorNonce }),
|
|
1576
|
+
};
|
|
1577
|
+
const attrText = Object.entries(attrs)
|
|
1578
|
+
.flatMap(([name, value]) => {
|
|
1579
|
+
if (value === undefined || value === false) {
|
|
1580
|
+
return [];
|
|
1581
|
+
}
|
|
1582
|
+
return value === true
|
|
1583
|
+
? [escapeHtmlAttribute(name)]
|
|
1584
|
+
: [`${escapeHtmlAttribute(name)}="${escapeHtmlAttribute(String(value))}"`];
|
|
1585
|
+
})
|
|
1586
|
+
.join(" ");
|
|
1587
|
+
const open = attrText === "" ? `<${descriptor.tag}>` : `<${descriptor.tag} ${attrText}>`;
|
|
1588
|
+
if (descriptor.tag === "meta" || descriptor.tag === "link" || descriptor.tag === "base") {
|
|
1589
|
+
return [open.slice(0, -1) + ">"];
|
|
1590
|
+
}
|
|
1591
|
+
return [`${open}${escapeHeadTextContent(descriptor.content ?? "")}</${descriptor.tag}>`];
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
function escapeHeadTextContent(value) {
|
|
1595
|
+
return value.replaceAll("<", "\\u003c");
|
|
1596
|
+
}
|
|
1597
|
+
function metadataString(value, path) {
|
|
1598
|
+
if (isMetadataScalar(value)) {
|
|
1599
|
+
return String(value);
|
|
1600
|
+
}
|
|
1601
|
+
throw new Error(`Invalid metadata field ${path}: expected string, number, or boolean.`);
|
|
1602
|
+
}
|
|
1603
|
+
function metadataKebabName(name) {
|
|
1604
|
+
return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
1605
|
+
}
|
|
1606
|
+
function viewportContent(viewport) {
|
|
1607
|
+
if (isMetadataScalar(viewport)) {
|
|
1608
|
+
return metadataString(viewport, "viewport");
|
|
1609
|
+
}
|
|
1610
|
+
return Object.entries(viewport)
|
|
1611
|
+
.flatMap(([key, value]) => {
|
|
1612
|
+
if (value === undefined || value === null || value === false) {
|
|
1613
|
+
return [];
|
|
1614
|
+
}
|
|
1615
|
+
return [`${metadataKebabName(key)}=${metadataString(value, `viewport.${key}`)}`];
|
|
1616
|
+
})
|
|
1617
|
+
.join(", ");
|
|
1618
|
+
}
|
|
1619
|
+
function themeColorTag(themeColor) {
|
|
1620
|
+
if (isMetadataScalar(themeColor)) {
|
|
1621
|
+
return `<meta name="theme-color" content="${escapeHtmlAttribute(metadataString(themeColor, "themeColor"))}">`;
|
|
1622
|
+
}
|
|
1623
|
+
const content = themeColor.color;
|
|
1624
|
+
if (!isMetadataScalar(content)) {
|
|
1625
|
+
throw new Error("Invalid metadata field themeColor.color: expected string, number, or boolean.");
|
|
1626
|
+
}
|
|
1627
|
+
const media = themeColor.media === undefined
|
|
1628
|
+
? ""
|
|
1629
|
+
: ` media="${escapeHtmlAttribute(metadataString(metadataScalarField(themeColor.media, "themeColor.media"), "themeColor.media"))}"`;
|
|
1630
|
+
return `<meta name="theme-color"${media} content="${escapeHtmlAttribute(metadataString(content, "themeColor.color"))}">`;
|
|
1631
|
+
}
|
|
1632
|
+
function metadataScalarField(value, path) {
|
|
1633
|
+
if (isMetadataScalar(value)) {
|
|
1634
|
+
return value;
|
|
1635
|
+
}
|
|
1636
|
+
throw new Error(`Invalid metadata field ${path}: expected string, number, or boolean.`);
|
|
1637
|
+
}
|
|
1638
|
+
function isMetadataScalar(value) {
|
|
1639
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
1640
|
+
}
|
|
1641
|
+
function openGraphImages(openGraph) {
|
|
1642
|
+
if (openGraph?.images !== undefined) {
|
|
1643
|
+
return openGraph.images.map((image, index) => metadataString(image, `openGraph.images.${index}`));
|
|
1644
|
+
}
|
|
1645
|
+
return openGraph?.image === undefined ? [] : [metadataString(openGraph.image, "openGraph.image")];
|
|
1646
|
+
}
|
|
1647
|
+
function robotsContent(robots) {
|
|
1648
|
+
if (typeof robots === "string") {
|
|
1649
|
+
return robots;
|
|
1650
|
+
}
|
|
1651
|
+
return [
|
|
1652
|
+
robots.index === false ? "noindex" : "index",
|
|
1653
|
+
robots.follow === false ? "nofollow" : "follow",
|
|
1654
|
+
].join(",");
|
|
1655
|
+
}
|
|
1656
|
+
function readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles) {
|
|
1657
|
+
const manifestSource = serverSourceFiles?.get(file);
|
|
1658
|
+
if (manifestSource !== undefined) {
|
|
1659
|
+
return Promise.resolve(manifestSource);
|
|
1660
|
+
}
|
|
1661
|
+
if (serverModuleCacheVersion === undefined) {
|
|
1662
|
+
return readFile(file, "utf8");
|
|
1663
|
+
}
|
|
1664
|
+
const key = `${serverModuleCacheVersion}:${file}`;
|
|
1665
|
+
const cached = serverSourceFileCache.get(key);
|
|
1666
|
+
if (cached !== undefined) {
|
|
1667
|
+
return cached;
|
|
1668
|
+
}
|
|
1669
|
+
const loaded = readFile(file, "utf8").catch((error) => {
|
|
1670
|
+
serverSourceFileCache.delete(key);
|
|
1671
|
+
throw error;
|
|
1672
|
+
});
|
|
1673
|
+
setBoundedCacheEntry(serverSourceFileCache, key, loaded, maxServerSourceFileCacheEntries);
|
|
1674
|
+
return loaded;
|
|
1675
|
+
}
|
|
1676
|
+
function hashText(text) {
|
|
1677
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
1678
|
+
}
|
|
1679
|
+
function setBoundedCacheEntry(cache, key, value, maxEntries) {
|
|
1680
|
+
if (cache.size >= maxEntries) {
|
|
1681
|
+
const oldestKey = cache.keys().next().value;
|
|
1682
|
+
if (oldestKey !== undefined) {
|
|
1683
|
+
cache.delete(oldestKey);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
cache.set(key, value);
|
|
1687
|
+
}
|
|
1688
|
+
//# sourceMappingURL=render.js.map
|