@rangojs/router 0.30.0 → 0.30.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/dist/bin/rango.js +1 -1
- package/dist/types/suspense-audit.d.ts +19 -1
- package/dist/types/vite/discovery/build-versions.d.ts +17 -13
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +1 -1
- package/dist/vite/index.js +35 -15
- package/package.json +1 -1
- package/skills/testing/response-routes.md +3 -2
- package/src/suspense-audit.ts +58 -11
- package/src/testing/render-route.tsx +5 -1
- package/src/vite/discovery/build-versions.ts +67 -26
- package/src/vite/plugins/stable-rsc-output.ts +1 -1
package/dist/bin/rango.js
CHANGED
|
@@ -1644,7 +1644,7 @@ var init_package = __esm({
|
|
|
1644
1644
|
"package.json"() {
|
|
1645
1645
|
package_default = {
|
|
1646
1646
|
name: "@rangojs/router",
|
|
1647
|
-
version: "0.30.
|
|
1647
|
+
version: "0.30.1",
|
|
1648
1648
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1649
1649
|
keywords: [
|
|
1650
1650
|
"react",
|
|
@@ -68,6 +68,19 @@ export interface BoundaryAudit {
|
|
|
68
68
|
/** The fallback's effect was cleaned up in this task (auditFallback). */
|
|
69
69
|
fallbackGone: boolean;
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* One loaded copy of this module. A test that re-imports the module after
|
|
73
|
+
* vi.resetModules() runs two, and the one its static imports use is not the
|
|
74
|
+
* one on `__rangoSuspenseAudit`. The router's unit-test lock resets and
|
|
75
|
+
* checks each (src/__tests__/kit/suspense-audit-lock.ts).
|
|
76
|
+
*/
|
|
77
|
+
export interface SuspenseAuditInstance {
|
|
78
|
+
/** The violation counters above zero and the last events, if any. */
|
|
79
|
+
violations(): string | undefined;
|
|
80
|
+
forget(): void;
|
|
81
|
+
/** Resolves once the judgments queued so far have run. */
|
|
82
|
+
settle(): Promise<void>;
|
|
83
|
+
}
|
|
71
84
|
export declare function createBoundaryAudit(boundary: string, segmentId: string): BoundaryAudit;
|
|
72
85
|
/**
|
|
73
86
|
* Render phase: the value this render hands the boundary.
|
|
@@ -153,5 +166,10 @@ export declare function auditTreeCause(cause: TreeUpdateCause): void;
|
|
|
153
166
|
* readers subscribe to, or through a pending promise read with use().
|
|
154
167
|
*/
|
|
155
168
|
export declare function auditTreeUpdate(root?: unknown): void;
|
|
156
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* Forget every tree, mount, hand-over and count seen so far: the root that
|
|
171
|
+
* held them is gone. A test unmounts its root, production never does
|
|
172
|
+
* (testing/render-route.tsx, src/__tests__/kit/suspense-audit-lock.ts).
|
|
173
|
+
* Creates no report: with the audit off there is none.
|
|
174
|
+
*/
|
|
157
175
|
export declare function forgetSuspenseAudit(): void;
|
|
@@ -194,22 +194,26 @@ export declare function isSourceMap(fileName: string): boolean;
|
|
|
194
194
|
* hashed by the routers that reach it (resolveRouterMembers), so nothing the
|
|
195
195
|
* map says is lost by skipping it.
|
|
196
196
|
*
|
|
197
|
-
* Works on the
|
|
198
|
-
* keeps the map in every router's hash: more
|
|
197
|
+
* Works on the module markers, so a minified server build (which has none),
|
|
198
|
+
* or a map no marker follows, keeps the map in every router's hash: more
|
|
199
|
+
* clearing, never less.
|
|
199
200
|
*/
|
|
200
201
|
export declare function stripServerReferenceMap(source: string): string;
|
|
201
202
|
/**
|
|
202
|
-
* Take the module paths out of a chunk's
|
|
203
|
+
* Take the module paths out of a chunk's module markers.
|
|
203
204
|
*
|
|
204
|
-
* The
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
205
|
+
* The path says where the build ran, not what it built: it is relative to the
|
|
206
|
+
* directory `vite build` was started from (`vite build apps/web` from a
|
|
207
|
+
* repository root prints other paths than `vite build` in the app), it holds
|
|
208
|
+
* the pnpm store directory with its peer suffix, and a virtual id is printed
|
|
209
|
+
* verbatim, absolute paths in its query included. The code after the marker
|
|
210
|
+
* is hashed either way.
|
|
211
|
+
*
|
|
212
|
+
* A `//#region` line is always a marker. A `// ` line is one only when it
|
|
213
|
+
* names a module of the chunk (`moduleIds`): a kept comment or a line of a
|
|
214
|
+
* multi-line string can start the same way, and is hashed.
|
|
211
215
|
*/
|
|
212
|
-
export declare function
|
|
216
|
+
export declare function stripModulePaths(source: string, moduleIds: readonly string[]): string;
|
|
213
217
|
/** One thing a version is a hash of: its name and the digest of its bytes. */
|
|
214
218
|
export type VersionInput = readonly [name: string, digest: string];
|
|
215
219
|
/** One bundle file, digested once for every router. */
|
|
@@ -237,8 +241,8 @@ export interface DigestedFile {
|
|
|
237
241
|
*
|
|
238
242
|
* Three things are taken out of a chunk's text first, each because it says
|
|
239
243
|
* where or how the build ran: the server-reference map's body
|
|
240
|
-
* ({@link stripServerReferenceMap}), the paths in
|
|
241
|
-
* ({@link
|
|
244
|
+
* ({@link stripServerReferenceMap}), the paths in module markers
|
|
245
|
+
* ({@link stripModulePaths}), and the hashed names of bundle files. A name
|
|
242
246
|
* carries the content hash of its file, and every chunk importing the file
|
|
243
247
|
* has the name in its bytes, so one router's change would otherwise move the
|
|
244
248
|
* digest of every chunk naming its chunk. Only names with a content hash are
|
|
@@ -41,7 +41,7 @@ export declare function canonicalAssetsManifestBinding(code: string): string | u
|
|
|
41
41
|
* (`ServerReferencesManager.claimMap`), which varies with the concurrent
|
|
42
42
|
* scan builds (e2e/test-app: 2 of 2 consecutive builds differed). Sorted
|
|
43
43
|
* here so a minified server build, where stripServerReferenceMap finds no
|
|
44
|
-
*
|
|
44
|
+
* module markers, still hashes the same bytes.
|
|
45
45
|
* 2. Local name of the assets-manifest import. plugin-rsc imports the manifest
|
|
46
46
|
* under three names; the bundler merges them into one import per chunk and
|
|
47
47
|
* keeps a different one from build to build (the cache-versions fixture:
|
package/dist/vite/index.js
CHANGED
|
@@ -7141,7 +7141,7 @@ function resolveVersionsFrom(table, routerId2) {
|
|
|
7141
7141
|
// package.json
|
|
7142
7142
|
var package_default = {
|
|
7143
7143
|
name: "@rangojs/router",
|
|
7144
|
-
version: "0.30.
|
|
7144
|
+
version: "0.30.1",
|
|
7145
7145
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
7146
7146
|
keywords: [
|
|
7147
7147
|
"react",
|
|
@@ -8053,20 +8053,39 @@ var SERVER_CSS_MODULE = /^virtual:vite-rsc\/css\?type=rsc&id=([^&]+)/;
|
|
|
8053
8053
|
function isSourceMap(fileName) {
|
|
8054
8054
|
return fileName.endsWith(".map");
|
|
8055
8055
|
}
|
|
8056
|
-
var
|
|
8057
|
-
|
|
8058
|
-
|
|
8056
|
+
var MODULE_MARKER = /^\/\/(#region)? (.+)$/gm;
|
|
8057
|
+
var SERVER_REFERENCE_MAP = new RegExp(
|
|
8058
|
+
`(^//(?:#region)? ${escapeRegExp(`\\0${SERVER_REFERENCES_MODULE_ID.slice(1)}`)}
|
|
8059
|
+
)[\\s\\S]*?(?=^//(?: |#endregion))`,
|
|
8060
|
+
"m"
|
|
8061
|
+
);
|
|
8059
8062
|
function stripServerReferenceMap(source) {
|
|
8060
|
-
|
|
8061
|
-
if (start === -1) return source;
|
|
8062
|
-
const bodyStart = start + SERVER_REFERENCES_REGION_START.length;
|
|
8063
|
-
const end = source.indexOf(REGION_END, bodyStart);
|
|
8064
|
-
if (end === -1) return source;
|
|
8065
|
-
return source.slice(0, bodyStart) + source.slice(end);
|
|
8063
|
+
return source.replace(SERVER_REFERENCE_MAP, "$1");
|
|
8066
8064
|
}
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8065
|
+
function printedModuleIds(moduleIds) {
|
|
8066
|
+
const printed = /* @__PURE__ */ new Set();
|
|
8067
|
+
for (const id of moduleIds) {
|
|
8068
|
+
if (!isAbsolutePath(id)) {
|
|
8069
|
+
printed.add(id.replaceAll("\0", "\\0"));
|
|
8070
|
+
continue;
|
|
8071
|
+
}
|
|
8072
|
+
const segments = id.replaceAll("\\", "/").split("/");
|
|
8073
|
+
for (let index = 0; index < segments.length; index++) {
|
|
8074
|
+
printed.add(segments.slice(index).join("/"));
|
|
8075
|
+
}
|
|
8076
|
+
}
|
|
8077
|
+
return printed;
|
|
8078
|
+
}
|
|
8079
|
+
function stripModulePaths(source, moduleIds) {
|
|
8080
|
+
let printed;
|
|
8081
|
+
return source.replace(
|
|
8082
|
+
MODULE_MARKER,
|
|
8083
|
+
(line, region, name) => {
|
|
8084
|
+
if (region) return "//#region";
|
|
8085
|
+
printed ??= printedModuleIds(moduleIds);
|
|
8086
|
+
return printed.has(name.replace(/^(?:\.\.\/)+/, "")) ? "//" : line;
|
|
8087
|
+
}
|
|
8088
|
+
);
|
|
8070
8089
|
}
|
|
8071
8090
|
var FILE_REFERENCE = "\0";
|
|
8072
8091
|
var FOREIGN_FILE = "~";
|
|
@@ -8118,8 +8137,9 @@ function digestBundle(bundle, projectRoot, read, label) {
|
|
|
8118
8137
|
};
|
|
8119
8138
|
}
|
|
8120
8139
|
const references = [];
|
|
8121
|
-
const text =
|
|
8122
|
-
stripServerReferenceMap(decoder3.decode(bytes))
|
|
8140
|
+
const text = stripModulePaths(
|
|
8141
|
+
stripServerReferenceMap(decoder3.decode(bytes)),
|
|
8142
|
+
chunk.moduleIds
|
|
8123
8143
|
);
|
|
8124
8144
|
const blanked = reference ? text.replace(reference, (name) => {
|
|
8125
8145
|
references.push(fileOfBaseName.get(name));
|
package/package.json
CHANGED
|
@@ -82,14 +82,15 @@ describe("api routes via dispatch", () => {
|
|
|
82
82
|
|
|
83
83
|
- trailing-slash redirects from the trie — a route with `trailingSlash: "never"` or `"always"` answers the other form with a 308 and the `Location` (query preserved). A route with no `trailingSlash` serves its URL with and without a trailing slash, with no redirect, as production does;
|
|
84
84
|
- the open-redirect guard on browser-followed redirects (a cross-origin `Location` is rewritten to the basename root unless `redirect(url, { external: true })`);
|
|
85
|
-
- content-negotiated routes (`Vary: Accept` appended);
|
|
85
|
+
- content-negotiated routes whose winning route is a response route (`Vary: Accept` appended);
|
|
86
86
|
- the reloads production answers inside the global middleware chain: a request whose `_rsc_v` names another deploy's document version (`setBuildVersions`, `./serve-shell-request.md`), and a `_rsc_partial` request whose `_rsc_rid` names another router (a cross-app navigation), get an `X-RSC-Reload` at the visitor's URL — on any route, a component route or an unmatched path included;
|
|
87
87
|
- cached response routes (`cache({ ... })` around a `path.json` etc.): hit, SWR, and tag writes. The cache write is scheduled via `waitUntil` (a microtask in tests), so flush microtasks between the seeding `dispatch` and the one that asserts the hit;
|
|
88
88
|
- `createRouter({ telemetry })` `request.start` / `request.end` / `request.error` events, all with one `requestId`, which a response route's `request.timeout` carries too (a rewritten pass's included), and `createRouter({ onError })` for cache-read/write failures.
|
|
89
89
|
|
|
90
90
|
## Caveats
|
|
91
91
|
|
|
92
|
-
- Hitting a COMPONENT (RSC) route throws a clear directive error: `dispatch` is for response routes + redirects + 404 +
|
|
92
|
+
- Hitting a COMPONENT (RSC) route throws a clear directive error: `dispatch` is for response routes + redirects + 404 + negotiation won by a response route, plus the global + route-level middleware guard stack on RESPONSE routes — it never renders React. Use Flight primitives or e2e to exercise component rendering.
|
|
93
|
+
- On a content-negotiated URL, any `Accept` the page route wins throws the same error. Serve that request with `serveShellRequest(router, url, { headers: { accept } })` (`./serve-shell-request.md`): it runs the production handler and answers the HTML document or the Flight stream, with `Vary: Accept`.
|
|
93
94
|
- A COMPONENT route's guard stack cannot run here. Assert it at e2e, or extract the middleware fn and unit-test it with `runMiddleware` (see `./middleware.md`).
|
|
94
95
|
- JSON serialization is bare, applied in `response-route-handler.ts`: a `path.json` handler that returns a value is serialized verbatim (`JSON.stringify(value)`, status 200, `application/json`) — no envelope. Returning or throwing a `Response` (e.g. `Response.json(x)`) uses the same control-flow path. Any other thrown error takes the error's status (`RouterError.status`, else 500, or a non-200 status already set upstream in the request pipeline). On a `path.json` route the body is an RFC 9457 problem+json object `{ title, status, detail, code }` (`application/problem+json;charset=utf-8`), where `code` is the `RouterError.code`, else `"INTERNAL"`, and the `type` member is omitted. On text/html/xml/md routes the body is `text/plain`: the `RouterError` message, the error message in dev, or `"Internal Server Error"` in production. Assert the shape matching what your handler returns.
|
|
95
96
|
- Setup: needs the preset (alias + virtual stubs) or a Vite-RSC env (see `./setup.md`); a bare router import throws on Vite virtuals.
|
package/src/suspense-audit.ts
CHANGED
|
@@ -233,10 +233,24 @@ const handedSeen = new Map<string, WeakSet<object>>();
|
|
|
233
233
|
// Thenables the router handed unread that React found already settled when
|
|
234
234
|
// it read them: it suspended on a value that was there. The one cause of a
|
|
235
235
|
// fallback the router owns (I3); consumed by the fallback it caused.
|
|
236
|
-
|
|
236
|
+
let needless = new WeakSet<object>();
|
|
237
237
|
|
|
238
238
|
let report: SuspenseAuditReport | undefined;
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* One loaded copy of this module. A test that re-imports the module after
|
|
242
|
+
* vi.resetModules() runs two, and the one its static imports use is not the
|
|
243
|
+
* one on `__rangoSuspenseAudit`. The router's unit-test lock resets and
|
|
244
|
+
* checks each (src/__tests__/kit/suspense-audit-lock.ts).
|
|
245
|
+
*/
|
|
246
|
+
export interface SuspenseAuditInstance {
|
|
247
|
+
/** The violation counters above zero and the last events, if any. */
|
|
248
|
+
violations(): string | undefined;
|
|
249
|
+
forget(): void;
|
|
250
|
+
/** Resolves once the judgments queued so far have run. */
|
|
251
|
+
settle(): Promise<void>;
|
|
252
|
+
}
|
|
253
|
+
|
|
240
254
|
function getReport(): SuspenseAuditReport {
|
|
241
255
|
if (report) return report;
|
|
242
256
|
const created: SuspenseAuditReport = {
|
|
@@ -277,9 +291,16 @@ function getReport(): SuspenseAuditReport {
|
|
|
277
291
|
};
|
|
278
292
|
report = created;
|
|
279
293
|
if (typeof window !== "undefined") {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
294
|
+
const target = window as unknown as {
|
|
295
|
+
__rangoSuspenseAudit?: SuspenseAuditReport;
|
|
296
|
+
__rangoSuspenseAudits?: SuspenseAuditInstance[];
|
|
297
|
+
};
|
|
298
|
+
target.__rangoSuspenseAudit = created;
|
|
299
|
+
(target.__rangoSuspenseAudits ??= []).push({
|
|
300
|
+
violations: () => violationSummary(created),
|
|
301
|
+
forget: forgetSuspenseAudit,
|
|
302
|
+
settle: () => new Promise((resolve) => afterMicrotasks(resolve)),
|
|
303
|
+
});
|
|
283
304
|
}
|
|
284
305
|
return created;
|
|
285
306
|
}
|
|
@@ -306,17 +327,35 @@ const RULES: Record<SuspenseAuditKind, [invariant: string, counter: Counter]> =
|
|
|
306
327
|
mutated: ["I7", "mutations"],
|
|
307
328
|
};
|
|
308
329
|
|
|
330
|
+
function eventLine(
|
|
331
|
+
kind: SuspenseAuditKind,
|
|
332
|
+
boundary: string,
|
|
333
|
+
detail: string,
|
|
334
|
+
): string {
|
|
335
|
+
return `${RULES[kind][0]} ${kind} at ${boundary}: ${detail}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function violationSummary(r: SuspenseAuditReport): string | undefined {
|
|
339
|
+
const counts = Object.values(RULES)
|
|
340
|
+
.filter(([, counter]) => r[counter] > 0)
|
|
341
|
+
.map(([invariant, counter]) => `${invariant} ${counter}: ${r[counter]}`);
|
|
342
|
+
if (counts.length === 0) return undefined;
|
|
343
|
+
const events = r.events.map(
|
|
344
|
+
(event) => ` ${eventLine(event.kind, event.boundary, event.detail)}`,
|
|
345
|
+
);
|
|
346
|
+
return [counts.join(", "), ...events].join("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
309
349
|
function violate(
|
|
310
350
|
kind: SuspenseAuditKind,
|
|
311
351
|
boundary: string,
|
|
312
352
|
detail: string,
|
|
313
353
|
): void {
|
|
314
|
-
const [invariant, counter] = RULES[kind];
|
|
315
354
|
const r = getReport();
|
|
316
|
-
r[
|
|
355
|
+
r[RULES[kind][1]] += 1;
|
|
317
356
|
r.events.push({ kind, boundary, detail, url: href(), at: performance.now() });
|
|
318
357
|
if (r.events.length > MAX_EVENTS) r.events.shift();
|
|
319
|
-
console.error(`${PREFIX} ${
|
|
358
|
+
console.error(`${PREFIX} ${eventLine(kind, boundary, detail)}`);
|
|
320
359
|
}
|
|
321
360
|
|
|
322
361
|
// Every hand-over, mount and fallback, for a debug run to read back. Call
|
|
@@ -617,9 +656,9 @@ let pendingRoot: PromiseLike<unknown> | undefined;
|
|
|
617
656
|
// I7: the fields of each segment object handed to React, at hand-over, and
|
|
618
657
|
// the segments themselves, weakly: the history cache keeps a tree's segments
|
|
619
658
|
// after the next tree drops them, and a write there is still a write.
|
|
620
|
-
|
|
659
|
+
let handedFields = new WeakMap<object, Record<string, unknown>>();
|
|
621
660
|
const handedSegments = new Set<WeakRef<object>>();
|
|
622
|
-
|
|
661
|
+
let trackedSegments = new WeakSet<object>();
|
|
623
662
|
|
|
624
663
|
function heldTree(): TreeAudit | undefined {
|
|
625
664
|
if (pendingRoot) {
|
|
@@ -1016,15 +1055,23 @@ export function auditTreeUpdate(root?: unknown): void {
|
|
|
1016
1055
|
}
|
|
1017
1056
|
}
|
|
1018
1057
|
|
|
1019
|
-
/**
|
|
1058
|
+
/**
|
|
1059
|
+
* Forget every tree, mount, hand-over and count seen so far: the root that
|
|
1060
|
+
* held them is gone. A test unmounts its root, production never does
|
|
1061
|
+
* (testing/render-route.tsx, src/__tests__/kit/suspense-audit-lock.ts).
|
|
1062
|
+
* Creates no report: with the audit off there is none.
|
|
1063
|
+
*/
|
|
1020
1064
|
export function forgetSuspenseAudit(): void {
|
|
1021
1065
|
held = undefined;
|
|
1022
1066
|
handedSegments.clear();
|
|
1067
|
+
handedFields = new WeakMap();
|
|
1068
|
+
trackedSegments = new WeakSet();
|
|
1069
|
+
needless = new WeakSet();
|
|
1023
1070
|
pendingRoot = undefined;
|
|
1024
1071
|
treeCause = undefined;
|
|
1025
1072
|
unmounted.clear();
|
|
1026
1073
|
remounted.length = 0;
|
|
1027
1074
|
settledSinceTree.clear();
|
|
1028
1075
|
mountCounts.clear();
|
|
1029
|
-
|
|
1076
|
+
report?.reset();
|
|
1030
1077
|
}
|
|
@@ -128,7 +128,7 @@ import { isUnderTestRunner } from "../runtime-env.js";
|
|
|
128
128
|
import { setupNavigationBridgeDelegatedPrefetch } from "../browser/navigation-bridge.js";
|
|
129
129
|
import { resetPrefetchObserverForTesting } from "../browser/prefetch/observer.js";
|
|
130
130
|
import type { PrefetchStrategy } from "../router/prefetch-default.js";
|
|
131
|
-
import { auditTreeCause } from "../suspense-audit.js";
|
|
131
|
+
import { auditTreeCause, forgetSuspenseAudit } from "../suspense-audit.js";
|
|
132
132
|
|
|
133
133
|
const TEST_ORIGIN = "http://localhost";
|
|
134
134
|
let activePrefetchRegistrations = 0;
|
|
@@ -1186,6 +1186,10 @@ export async function renderRoute(
|
|
|
1186
1186
|
...makeMetadata(url.pathname, initialSegments, initialMatch.params),
|
|
1187
1187
|
defaultPrefetch: options.defaultPrefetch,
|
|
1188
1188
|
};
|
|
1189
|
+
// The suspense audit holds one document's tree and mounts: a root an earlier
|
|
1190
|
+
// renderRoute mounted is gone (a test unmounts its root, production never
|
|
1191
|
+
// does). A no-op unless this repo's tests run the audit.
|
|
1192
|
+
forgetSuspenseAudit();
|
|
1189
1193
|
const initialTree = await renderSegments(initialSegments, {
|
|
1190
1194
|
outletPending: options.outletPending,
|
|
1191
1195
|
});
|
|
@@ -593,11 +593,23 @@ export function isSourceMap(fileName: string): boolean {
|
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
/**
|
|
596
|
-
* The bundler's
|
|
597
|
-
*
|
|
596
|
+
* The bundler's module marker: the comment line before each module's code in
|
|
597
|
+
* an unminified chunk, naming the module (`\0` printed as two characters).
|
|
598
|
+
* rolldown before 1.2.15 wraps the code in `//#region <id>` and
|
|
599
|
+
* `//#endregion`; 1.2.15 (rolldown/rolldown#11235) prints only `// <id>`
|
|
600
|
+
* where the module starts.
|
|
598
601
|
*/
|
|
599
|
-
const
|
|
600
|
-
|
|
602
|
+
const MODULE_MARKER = /^\/\/(#region)? (.+)$/gm;
|
|
603
|
+
/**
|
|
604
|
+
* plugin-rsc's server-reference map: its marker in either form (group 1), and
|
|
605
|
+
* its body up to the next `// ` line or the region's end. The body stops at
|
|
606
|
+
* any `// ` line, a comment of its own included, so it never takes in the
|
|
607
|
+
* code of the module after it.
|
|
608
|
+
*/
|
|
609
|
+
const SERVER_REFERENCE_MAP = new RegExp(
|
|
610
|
+
`(^//(?:#region)? ${escapeRegExp(`\\0${SERVER_REFERENCES_MODULE_ID.slice(1)}`)}\n)[\\s\\S]*?(?=^//(?: |#endregion))`,
|
|
611
|
+
"m",
|
|
612
|
+
);
|
|
601
613
|
|
|
602
614
|
/**
|
|
603
615
|
* Drop the body of the server-reference map from a chunk's source.
|
|
@@ -609,33 +621,61 @@ const REGION_END = "//#endregion";
|
|
|
609
621
|
* hashed by the routers that reach it (resolveRouterMembers), so nothing the
|
|
610
622
|
* map says is lost by skipping it.
|
|
611
623
|
*
|
|
612
|
-
* Works on the
|
|
613
|
-
* keeps the map in every router's hash: more
|
|
624
|
+
* Works on the module markers, so a minified server build (which has none),
|
|
625
|
+
* or a map no marker follows, keeps the map in every router's hash: more
|
|
626
|
+
* clearing, never less.
|
|
614
627
|
*/
|
|
615
628
|
export function stripServerReferenceMap(source: string): string {
|
|
616
|
-
|
|
617
|
-
if (start === -1) return source;
|
|
618
|
-
const bodyStart = start + SERVER_REFERENCES_REGION_START.length;
|
|
619
|
-
const end = source.indexOf(REGION_END, bodyStart);
|
|
620
|
-
if (end === -1) return source;
|
|
621
|
-
return source.slice(0, bodyStart) + source.slice(end);
|
|
629
|
+
return source.replace(SERVER_REFERENCE_MAP, "$1");
|
|
622
630
|
}
|
|
623
631
|
|
|
624
|
-
|
|
632
|
+
/**
|
|
633
|
+
* Every way the bundler can print one of `moduleIds` in a marker. rolldown
|
|
634
|
+
* prints a path relative to its working directory (`stabilize_id`), so any
|
|
635
|
+
* trailing run of the path's segments, and any other id as it is.
|
|
636
|
+
*/
|
|
637
|
+
function printedModuleIds(moduleIds: readonly string[]): Set<string> {
|
|
638
|
+
const printed = new Set<string>();
|
|
639
|
+
for (const id of moduleIds) {
|
|
640
|
+
if (!isAbsolutePath(id)) {
|
|
641
|
+
printed.add(id.replaceAll("\0", "\\0"));
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const segments = id.replaceAll("\\", "/").split("/");
|
|
645
|
+
for (let index = 0; index < segments.length; index++) {
|
|
646
|
+
printed.add(segments.slice(index).join("/"));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return printed;
|
|
650
|
+
}
|
|
625
651
|
|
|
626
652
|
/**
|
|
627
|
-
* Take the module paths out of a chunk's
|
|
653
|
+
* Take the module paths out of a chunk's module markers.
|
|
654
|
+
*
|
|
655
|
+
* The path says where the build ran, not what it built: it is relative to the
|
|
656
|
+
* directory `vite build` was started from (`vite build apps/web` from a
|
|
657
|
+
* repository root prints other paths than `vite build` in the app), it holds
|
|
658
|
+
* the pnpm store directory with its peer suffix, and a virtual id is printed
|
|
659
|
+
* verbatim, absolute paths in its query included. The code after the marker
|
|
660
|
+
* is hashed either way.
|
|
628
661
|
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
* prints other paths than `vite build` in the app), it holds the pnpm store
|
|
633
|
-
* directory with its peer suffix, and a virtual id is printed verbatim,
|
|
634
|
-
* absolute paths in its query included. The code under the comment is hashed
|
|
635
|
-
* either way.
|
|
662
|
+
* A `//#region` line is always a marker. A `// ` line is one only when it
|
|
663
|
+
* names a module of the chunk (`moduleIds`): a kept comment or a line of a
|
|
664
|
+
* multi-line string can start the same way, and is hashed.
|
|
636
665
|
*/
|
|
637
|
-
export function
|
|
638
|
-
|
|
666
|
+
export function stripModulePaths(
|
|
667
|
+
source: string,
|
|
668
|
+
moduleIds: readonly string[],
|
|
669
|
+
): string {
|
|
670
|
+
let printed: Set<string> | undefined;
|
|
671
|
+
return source.replace(
|
|
672
|
+
MODULE_MARKER,
|
|
673
|
+
(line, region: string | undefined, name: string) => {
|
|
674
|
+
if (region) return "//#region";
|
|
675
|
+
printed ??= printedModuleIds(moduleIds);
|
|
676
|
+
return printed.has(name.replace(/^(?:\.\.\/)+/, "")) ? "//" : line;
|
|
677
|
+
},
|
|
678
|
+
);
|
|
639
679
|
}
|
|
640
680
|
|
|
641
681
|
/** One thing a version is a hash of: its name and the digest of its bytes. */
|
|
@@ -676,8 +716,8 @@ const FOREIGN_FILE = "~";
|
|
|
676
716
|
*
|
|
677
717
|
* Three things are taken out of a chunk's text first, each because it says
|
|
678
718
|
* where or how the build ran: the server-reference map's body
|
|
679
|
-
* ({@link stripServerReferenceMap}), the paths in
|
|
680
|
-
* ({@link
|
|
719
|
+
* ({@link stripServerReferenceMap}), the paths in module markers
|
|
720
|
+
* ({@link stripModulePaths}), and the hashed names of bundle files. A name
|
|
681
721
|
* carries the content hash of its file, and every chunk importing the file
|
|
682
722
|
* has the name in its bytes, so one router's change would otherwise move the
|
|
683
723
|
* digest of every chunk naming its chunk. Only names with a content hash are
|
|
@@ -756,8 +796,9 @@ export function digestBundle(
|
|
|
756
796
|
};
|
|
757
797
|
}
|
|
758
798
|
const references: string[] = [];
|
|
759
|
-
const text =
|
|
799
|
+
const text = stripModulePaths(
|
|
760
800
|
stripServerReferenceMap(decoder.decode(bytes)),
|
|
801
|
+
chunk.moduleIds,
|
|
761
802
|
);
|
|
762
803
|
const blanked = reference
|
|
763
804
|
? text.replace(reference, (name) => {
|
|
@@ -121,7 +121,7 @@ export function canonicalAssetsManifestBinding(
|
|
|
121
121
|
* (`ServerReferencesManager.claimMap`), which varies with the concurrent
|
|
122
122
|
* scan builds (e2e/test-app: 2 of 2 consecutive builds differed). Sorted
|
|
123
123
|
* here so a minified server build, where stripServerReferenceMap finds no
|
|
124
|
-
*
|
|
124
|
+
* module markers, still hashes the same bytes.
|
|
125
125
|
* 2. Local name of the assets-manifest import. plugin-rsc imports the manifest
|
|
126
126
|
* under three names; the bundler merges them into one import per chunk and
|
|
127
127
|
* keeps a different one from build to build (the cache-versions fixture:
|