@webjsdev/server 0.8.88 → 0.8.90
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/package.json +1 -1
- package/src/asset-hash.js +44 -3
- package/src/build-epoch.js +50 -0
- package/src/conditional-get.js +33 -1
- package/src/context.js +3 -3
- package/src/dev/handler.js +65 -29
- package/src/dev/helpers.js +91 -30
- package/src/dev/vendor-retry.js +61 -0
- package/src/dev-import.js +38 -1
- package/src/headers.js +6 -1
- package/src/importmap.js +55 -11
- package/src/listener-bun.js +27 -6
- package/src/listener-core.js +52 -5
- package/src/rate-limit.js +0 -6
- package/src/session.js +1 -1
- package/src/ssr/env-shim.js +48 -7
- package/src/ssr/head.js +37 -16
- package/src/ssr/preloads.js +88 -14
- package/src/ssr/render.js +36 -74
- package/src/ssr/responses.js +4 -8
- package/src/ssr/shipped-modules.js +113 -0
package/src/ssr/render.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
2
1
|
import { renderToString, isNotFound, isRedirect, isForbidden, isUnauthorized } from '@webjsdev/core';
|
|
3
2
|
import { vendorPreloadTargets } from '../importmap.js';
|
|
4
3
|
import {
|
|
5
|
-
componentPreloads,
|
|
4
|
+
componentPreloads, pagePreloadHints, toUrlPath,
|
|
6
5
|
} from './preloads.js';
|
|
6
|
+
import { shippedBootModules } from './shipped-modules.js';
|
|
7
7
|
import { seedingEnabled, collectSeeds, buildSeedScript, SEED_DROP_BLOCK } from '../action-seed.js';
|
|
8
8
|
import {
|
|
9
9
|
readRevalidate,
|
|
@@ -830,7 +830,7 @@ async function ssrBoundaryHtml(file, heading, opts) {
|
|
|
830
830
|
moduleUrls = [];
|
|
831
831
|
}
|
|
832
832
|
}
|
|
833
|
-
const nonce =
|
|
833
|
+
const nonce = getNonce();
|
|
834
834
|
return wrapInDocument(body, {
|
|
835
835
|
metadata: { title: heading.replace(/^\d+:\s*/, '') },
|
|
836
836
|
moduleUrls,
|
|
@@ -900,7 +900,7 @@ async function ssrNotFoundHtml(notFoundFile, opts) {
|
|
|
900
900
|
moduleUrls = [];
|
|
901
901
|
}
|
|
902
902
|
}
|
|
903
|
-
const nonce =
|
|
903
|
+
const nonce = getNonce();
|
|
904
904
|
return wrapInDocument(body, {
|
|
905
905
|
metadata: { title: 'Not found' },
|
|
906
906
|
moduleUrls,
|
|
@@ -1113,37 +1113,19 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
1113
1113
|
// lazy loader instead of imported, exactly as a shipping module's served
|
|
1114
1114
|
// source defers its side-effect import of one, so the boot never fetches
|
|
1115
1115
|
// it up front and a tag revealed later still loads on first sight.
|
|
1116
|
-
|
|
1117
|
-
|
|
1116
|
+
//
|
|
1117
|
+
// The assembly itself lives in `shipped-modules.js` (#1644), shared with
|
|
1118
|
+
// the Node listener's 103 Early Hints and memoized per route object and
|
|
1119
|
+
// build epoch; nothing below mutates `moduleUrls`, which is what lets the
|
|
1120
|
+
// memoized array be handed out as is.
|
|
1118
1121
|
const lazyFiles = opts.lazyComponentFiles;
|
|
1119
|
-
const moduleUrls =
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
const tags = lazyFiles && lazyFiles.get(abs);
|
|
1127
|
-
if (tags) { for (const t of tags) frontierLazy[t] = u; return; }
|
|
1128
|
-
if (!seen.has(u)) { seen.add(u); moduleUrls.push(u); }
|
|
1129
|
-
};
|
|
1130
|
-
for (const f of [route.file, ...route.layouts]) {
|
|
1131
|
-
if (inert && inert.has(f)) continue;
|
|
1132
|
-
const emit = importOnly && importOnly.get(f);
|
|
1133
|
-
if (emit) emit.forEach(push);
|
|
1134
|
-
else push(f);
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
// instrumentation-client.{js,ts} (#848): import it FIRST in the client boot
|
|
1138
|
-
// so it runs before app modules (Next's instrumentation-client ordering).
|
|
1139
|
-
// It ships even on an otherwise-static page (the app opted into client work),
|
|
1140
|
-
// so it is prepended AFTER the component/page modules are collected.
|
|
1141
|
-
if (opts.instrumentationClient) {
|
|
1142
|
-
const u = toUrlPath(opts.instrumentationClient, opts.appDir);
|
|
1143
|
-
const i = moduleUrls.indexOf(u);
|
|
1144
|
-
if (i !== -1) moduleUrls.splice(i, 1);
|
|
1145
|
-
moduleUrls.unshift(u);
|
|
1146
|
-
}
|
|
1122
|
+
const { moduleUrls, frontierLazy } = shippedBootModules(route, {
|
|
1123
|
+
appDir: opts.appDir,
|
|
1124
|
+
inert: opts.inertRouteModules,
|
|
1125
|
+
importOnly: opts.importOnlyRouteModules,
|
|
1126
|
+
lazyFiles,
|
|
1127
|
+
instrumentationClient: opts.instrumentationClient,
|
|
1128
|
+
});
|
|
1147
1129
|
// Emit <link rel="modulepreload"> for every custom element that
|
|
1148
1130
|
// actually rendered PLUS their transitive dependencies (from the
|
|
1149
1131
|
// module graph). URLs are deduplicated so the browser never sees
|
|
@@ -1153,48 +1135,28 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
1153
1135
|
const { eager: eagerComponents, lazy: renderedLazy } =
|
|
1154
1136
|
componentPreloads(suspenseCtx.usedComponents, opts.appDir, opts.elidableComponents, lazyFiles);
|
|
1155
1137
|
const lazyComponents = { ...frontierLazy, ...renderedLazy };
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
// entries `[route.file, ...route.layouts]`. Rooting at the raw entries would
|
|
1166
|
-
// walk a dropped page's SSR-only subtree (a direct import OR a relative
|
|
1167
|
-
// helper) and hint a `modulepreload` for a module nothing that ships imports,
|
|
1168
|
-
// an over-fetch (#780, the app-module analog of the #754 vendor over-fetch).
|
|
1169
|
-
const shippedRoots = moduleUrls.map((u) =>
|
|
1170
|
-
resolve(opts.appDir, u.startsWith('/') ? u.slice(1) : u));
|
|
1171
|
-
const preloads = deduplicatedPreloads(
|
|
1172
|
-
eagerComponents,
|
|
1138
|
+
// Both preload passes (the app-module hints and the vendor hints, #754)
|
|
1139
|
+
// root at the BOOT's shipped set plus the rendered components and skip a
|
|
1140
|
+
// lazy component (#1524) or an elided one and the subtree only it reaches;
|
|
1141
|
+
// `pagePreloadHints` walks that closure ONCE for both (#1644), where the
|
|
1142
|
+
// two passes used to walk it twice. The over-fetch rules (#780, #754) are
|
|
1143
|
+
// documented on the helper.
|
|
1144
|
+
const hints = pagePreloadHints({
|
|
1145
|
+
graph: opts.moduleGraph,
|
|
1146
|
+
appDir: opts.appDir,
|
|
1173
1147
|
moduleUrls,
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
opts.serverFiles,
|
|
1178
|
-
|
|
1179
|
-
|
|
1148
|
+
eagerComponents,
|
|
1149
|
+
elidableComponents: opts.elidableComponents,
|
|
1150
|
+
lazyFiles,
|
|
1151
|
+
serverFiles: opts.serverFiles,
|
|
1152
|
+
});
|
|
1153
|
+
const preloads = hints.preloads;
|
|
1180
1154
|
// Vendor modulepreload (#754): flatten the npm CDN waterfall by hinting the
|
|
1181
|
-
// vendor URLs the page's SHIPPED modules actually import, fetched in
|
|
1182
|
-
// instead of discovered level by level.
|
|
1183
|
-
|
|
1184
|
-
// (a page's SSR-only direct import OR its SSR-only relative helper) is never
|
|
1185
|
-
// preloaded (no over-fetch).
|
|
1186
|
-
const vendorPreloads = vendorPreloadTargets(
|
|
1187
|
-
reachedVendorSpecifiers(
|
|
1188
|
-
opts.moduleGraph,
|
|
1189
|
-
shippedRoots,
|
|
1190
|
-
eagerComponents,
|
|
1191
|
-
opts.appDir,
|
|
1192
|
-
preloadSkip,
|
|
1193
|
-
opts.serverFiles,
|
|
1194
|
-
),
|
|
1195
|
-
);
|
|
1155
|
+
// vendor URLs the page's SHIPPED modules actually import, fetched in
|
|
1156
|
+
// parallel instead of discovered level by level.
|
|
1157
|
+
const vendorPreloads = vendorPreloadTargets(hints.vendorSpecifiers);
|
|
1196
1158
|
// Extract CSP nonce from request headers (if present).
|
|
1197
|
-
const nonce =
|
|
1159
|
+
const nonce = getNonce();
|
|
1198
1160
|
const wrapOpts = {
|
|
1199
1161
|
metadata,
|
|
1200
1162
|
moduleUrls,
|
|
@@ -1342,7 +1304,7 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
1342
1304
|
// Error paths still need to honor the request's CSP nonce so the
|
|
1343
1305
|
// error page's boot scripts (when moduleUrls is non-empty) and
|
|
1344
1306
|
// the meta csp-nonce tag both pass strict-CSP enforcement.
|
|
1345
|
-
const errNonce =
|
|
1307
|
+
const errNonce = getNonce();
|
|
1346
1308
|
// One dedup set for the whole 500 path: the walk below and the
|
|
1347
1309
|
// global-error attempt after it collapse REPEATS of the same cause (a
|
|
1348
1310
|
// shared layout that fails every attempt) while still reporting a
|
package/src/ssr/responses.js
CHANGED
|
@@ -16,19 +16,15 @@ import { escapeAttr, escapeHtml } from './escape.js';
|
|
|
16
16
|
* scope. Delegates to `cspNonce()`, which returns the per-request nonce
|
|
17
17
|
* the handler MINTED when CSP is enabled (issue #233), or, as a fallback,
|
|
18
18
|
* the nonce parsed from an inbound `Content-Security-Policy` request
|
|
19
|
-
* header (
|
|
19
|
+
* header (a proxy that mints its own). Using the same source as the
|
|
20
20
|
* `Content-Security-Policy` response header is what guarantees the inline
|
|
21
21
|
* boot script, the importmap, the modulepreload hints, and the header all
|
|
22
|
-
* carry the EXACT same nonce: one minted value, no drift.
|
|
22
|
+
* carry the EXACT same nonce: one minted value, no drift. The value comes
|
|
23
|
+
* from the request-scoped AsyncLocalStorage store, so it takes no request.
|
|
23
24
|
*
|
|
24
|
-
* `req` is accepted (and ignored) so existing call sites stay unchanged;
|
|
25
|
-
* the value comes from the request-scoped AsyncLocalStorage store, not
|
|
26
|
-
* the argument.
|
|
27
|
-
*
|
|
28
|
-
* @param {Request} [_req]
|
|
29
25
|
* @returns {string | undefined}
|
|
30
26
|
*/
|
|
31
|
-
export function getNonce(
|
|
27
|
+
export function getNonce() {
|
|
32
28
|
return cspNonce() || undefined;
|
|
33
29
|
}
|
|
34
30
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boot's shipped-module assembly, shared by the SSR render and the 103
|
|
3
|
+
* Early Hints (#1644).
|
|
4
|
+
*
|
|
5
|
+
* For a matched route the client boot imports the page and its layouts MINUS
|
|
6
|
+
* the inert ones (a module that does no client work, #170), with an
|
|
7
|
+
* IMPORT-ONLY module (#605) replaced by the component frontier it exists to
|
|
8
|
+
* import, and a `static lazy` component in that frontier (#1524) diverted to
|
|
9
|
+
* the lazy loader instead of imported. `ssr/render.js` derived that list per
|
|
10
|
+
* request, and the Node listener's `routeFor` (the 103 Early Hints) carried a
|
|
11
|
+
* second copy of the same loop that had drifted: it knew nothing of lazy
|
|
12
|
+
* files, so a lazy frontier component was early-hinted although the boot
|
|
13
|
+
* never imports it.
|
|
14
|
+
*
|
|
15
|
+
* One function now owns the assembly, memoized per ROUTE OBJECT in a WeakMap
|
|
16
|
+
* (the route table is rebuilt per build, so a stale entry dies with its
|
|
17
|
+
* route, and a dynamic route cannot grow the memo the way a pathname key
|
|
18
|
+
* would) and validated against the build epoch (the inert / import-only /
|
|
19
|
+
* lazy sets are per-build inputs). The hinted url list, the same modules
|
|
20
|
+
* through `withBasePath` then `withAssetHash`, is memoized beside it so a
|
|
21
|
+
* warm GET costs one WeakMap lookup.
|
|
22
|
+
*/
|
|
23
|
+
import { buildEpoch } from '../build-epoch.js';
|
|
24
|
+
import { withBasePath } from '../base-path.js';
|
|
25
|
+
import { withAssetHash, assetHashMisses } from '../asset-hash.js';
|
|
26
|
+
import { toUrlPath } from './preloads.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} ShippedDeps
|
|
30
|
+
* @property {string} appDir
|
|
31
|
+
* @property {Set<string> | undefined} inert inert page / layout files (never shipped)
|
|
32
|
+
* @property {Map<string, string[]> | undefined} importOnly import-only module -> the component files emitted in its place
|
|
33
|
+
* @property {Map<string, string[]> | undefined} lazyFiles lazy component file -> its tags
|
|
34
|
+
* @property {string | undefined} instrumentationClient `instrumentation-client.*`, imported first when present
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {object} ShippedModules
|
|
39
|
+
* @property {string[]} moduleUrls url paths the boot imports, in import order
|
|
40
|
+
* @property {Record<string, string>} frontierLazy tag -> url for lazy components diverted out of the frontier
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** @type {WeakMap<object, { epoch: number, value: ShippedModules, hinted: string[] | null, hintedBasePath: string }>} */
|
|
44
|
+
const MEMO = new WeakMap();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The modules the client boot imports for `route`, plus the lazy frontier it
|
|
48
|
+
* registers instead. Same object back for the same route within a build.
|
|
49
|
+
*
|
|
50
|
+
* @param {{ file: string, layouts: string[] }} route the matched route (its object identity is the memo key)
|
|
51
|
+
* @param {ShippedDeps} deps
|
|
52
|
+
* @returns {ShippedModules}
|
|
53
|
+
*/
|
|
54
|
+
export function shippedBootModules(route, deps) {
|
|
55
|
+
const hit = MEMO.get(route);
|
|
56
|
+
if (hit && hit.epoch === buildEpoch()) return hit.value;
|
|
57
|
+
const { appDir, inert, importOnly, lazyFiles, instrumentationClient } = deps;
|
|
58
|
+
/** @type {string[]} */
|
|
59
|
+
const moduleUrls = [];
|
|
60
|
+
/** @type {Record<string, string>} */
|
|
61
|
+
const frontierLazy = {};
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const push = (/** @type {string} */ abs) => {
|
|
64
|
+
const u = toUrlPath(abs, appDir);
|
|
65
|
+
const tags = lazyFiles && lazyFiles.get(abs);
|
|
66
|
+
if (tags) { for (const t of tags) frontierLazy[t] = u; return; }
|
|
67
|
+
if (!seen.has(u)) { seen.add(u); moduleUrls.push(u); }
|
|
68
|
+
};
|
|
69
|
+
for (const f of [route.file, ...route.layouts]) {
|
|
70
|
+
if (inert && inert.has(f)) continue;
|
|
71
|
+
const emit = importOnly && importOnly.get(f);
|
|
72
|
+
if (emit) emit.forEach(push);
|
|
73
|
+
else push(f);
|
|
74
|
+
}
|
|
75
|
+
// instrumentation-client.{js,ts} (#848): import it FIRST in the client boot
|
|
76
|
+
// so it runs before app modules (Next's instrumentation-client ordering).
|
|
77
|
+
// It ships even on an otherwise-static page (the app opted into client work).
|
|
78
|
+
if (instrumentationClient) {
|
|
79
|
+
const u = toUrlPath(instrumentationClient, appDir);
|
|
80
|
+
const i = moduleUrls.indexOf(u);
|
|
81
|
+
if (i !== -1) moduleUrls.splice(i, 1);
|
|
82
|
+
moduleUrls.unshift(u);
|
|
83
|
+
}
|
|
84
|
+
const value = { moduleUrls, frontierLazy };
|
|
85
|
+
MEMO.set(route, { epoch: buildEpoch(), value, hinted: null, hintedBasePath: '' });
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The 103 Early Hints url list for `route`: the SAME modules the boot
|
|
91
|
+
* imports, through `withBasePath` then `withAssetHash`, so the hint warms
|
|
92
|
+
* exactly the url the body requests (#243). Memoized beside the assembly.
|
|
93
|
+
*
|
|
94
|
+
* @param {{ file: string, layouts: string[] }} route
|
|
95
|
+
* @param {ShippedDeps} deps
|
|
96
|
+
* @param {string} basePath the normalized `webjs.basePath` (`''` at a root mount)
|
|
97
|
+
* @returns {string[]}
|
|
98
|
+
*/
|
|
99
|
+
export function hintedBootModules(route, deps, basePath) {
|
|
100
|
+
const { moduleUrls } = shippedBootModules(route, deps);
|
|
101
|
+
const entry = /** @type {NonNullable<ReturnType<typeof MEMO.get>>} */ (MEMO.get(route));
|
|
102
|
+
if (entry.hinted && entry.hintedBasePath === basePath) return entry.hinted;
|
|
103
|
+
// Not stored when a hash read failed transiently while computing it
|
|
104
|
+
// (`assetHashMisses` moved): the list is served with the un-fingerprinted
|
|
105
|
+
// url, and the next request re-attempts the read (see `asset-hash.js`).
|
|
106
|
+
const missesBefore = assetHashMisses();
|
|
107
|
+
const hinted = moduleUrls.map((u) => withAssetHash(withBasePath(u, basePath), basePath));
|
|
108
|
+
if (assetHashMisses() === missesBefore) {
|
|
109
|
+
entry.hinted = hinted;
|
|
110
|
+
entry.hintedBasePath = basePath;
|
|
111
|
+
}
|
|
112
|
+
return hinted;
|
|
113
|
+
}
|