@webjsdev/server 0.8.89 → 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/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 +51 -3
- package/src/listener-bun.js +27 -6
- package/src/listener-core.js +52 -5
- package/src/ssr/env-shim.js +48 -7
- package/src/ssr/head.js +34 -13
- package/src/ssr/preloads.js +88 -14
- package/src/ssr/render.js +32 -70
- 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,
|
|
@@ -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,46 +1135,26 @@ 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
1159
|
const nonce = getNonce();
|
|
1198
1160
|
const wrapOpts = {
|
|
@@ -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
|
+
}
|