@rangojs/router 0.19.0 → 0.20.0
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/types/cache/cache-exec-scope.d.ts +6 -0
- package/dist/types/cache/cache-tag.d.ts +24 -1
- package/dist/types/server/context.d.ts +38 -1
- package/dist/types/ssr/entry-preload-priority.d.ts +69 -0
- package/dist/types/ssr/index.d.ts +1 -0
- package/dist/types/ssr/preinit-client-references.d.ts +17 -5
- package/dist/types/vite/plugin-types.d.ts +20 -6
- package/dist/types/vite/plugins/virtual-entries.d.ts +3 -1
- package/dist/types/vite/utils/client-chunks.d.ts +22 -2
- package/dist/vite/index.js +43 -10
- package/package.json +1 -1
- package/skills/bundle-analysis/SKILL.md +1 -1
- package/skills/cache-guide/SKILL.md +3 -3
- package/skills/caching/SKILL.md +4 -4
- package/skills/parallel/SKILL.md +1 -1
- package/skills/rango/SKILL.md +2 -2
- package/skills/router-setup/SKILL.md +8 -2
- package/skills/use-cache/SKILL.md +8 -5
- package/src/cache/cache-exec-scope.ts +6 -0
- package/src/cache/cache-runtime.ts +5 -0
- package/src/cache/cache-tag.ts +104 -0
- package/src/router/loader-resolution.ts +15 -3
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/server/context.ts +75 -1
- package/src/server/request-context.ts +11 -3
- package/src/ssr/entry-preload-priority.ts +308 -0
- package/src/ssr/index.tsx +46 -9
- package/src/ssr/preinit-client-references.ts +17 -5
- package/src/vite/plugin-types.ts +20 -6
- package/src/vite/plugins/virtual-entries.ts +9 -2
- package/src/vite/rango.ts +2 -0
- package/src/vite/utils/client-chunks.ts +80 -14
package/src/ssr/index.tsx
CHANGED
|
@@ -2,6 +2,10 @@ import React from "react";
|
|
|
2
2
|
import { createSsrRootComponent, deserializeSsrPayload } from "./ssr-root.js";
|
|
3
3
|
import { injectRSCPayloadEager } from "./inject-rsc-eager.js";
|
|
4
4
|
import { runWithPreinitNonce } from "./preinit-client-references.js";
|
|
5
|
+
import {
|
|
6
|
+
entryPreloadPriorityTransform,
|
|
7
|
+
rewriteEntryPreloadPriority,
|
|
8
|
+
} from "./entry-preload-priority.js";
|
|
5
9
|
import {
|
|
6
10
|
POST_QUIESCE_TASK_HOPS,
|
|
7
11
|
SHELL_CAPTURE_MAX_WAIT_MS,
|
|
@@ -189,6 +193,7 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
189
193
|
* undefined keeps the inline bootstrap verbatim, so a custom SSR entry that
|
|
190
194
|
* never installed the preinit hook cannot drift into the half-converted
|
|
191
195
|
* state on upgrade (the generated entry always passes an explicit value).
|
|
196
|
+
* Any other value throws when the handler is created.
|
|
192
197
|
*/
|
|
193
198
|
headScripts?: HeadScriptsOption;
|
|
194
199
|
|
|
@@ -521,9 +526,23 @@ const MISSING_BOOTSTRAP_MSG =
|
|
|
521
526
|
* getClientEntryUrl only counts under an explicit `headScripts: "preinit"` —
|
|
522
527
|
* any other headScripts keeps the inline path, so its presence alone is a
|
|
523
528
|
* misconfiguration worth flagging rather than silently ignoring.
|
|
529
|
+
* An unknown headScripts value throws: a custom SSR entry bypasses the
|
|
530
|
+
* rango({ headScripts }) check in getVirtualEntrySSR, and a typo such as
|
|
531
|
+
* "prenit" would otherwise run the inline path as if it were "preload".
|
|
532
|
+
* undefined stays valid (see {@link resolveBootstrap}).
|
|
524
533
|
*/
|
|
525
534
|
function assertBootstrapDeps(deps: SSRDependencies): void {
|
|
526
|
-
const
|
|
535
|
+
const { headScripts } = deps;
|
|
536
|
+
if (
|
|
537
|
+
headScripts !== undefined &&
|
|
538
|
+
headScripts !== "preinit" &&
|
|
539
|
+
headScripts !== "preload"
|
|
540
|
+
) {
|
|
541
|
+
throw new Error(
|
|
542
|
+
`[ssr] headScripts must be "preinit" or "preload", received ${JSON.stringify(headScripts)}`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
const preinit = headScripts === "preinit";
|
|
527
546
|
if (deps.getClientEntryUrl && !preinit) {
|
|
528
547
|
console.warn(
|
|
529
548
|
'[ssr] getClientEntryUrl is ignored without headScripts: "preinit"; ' +
|
|
@@ -550,13 +569,18 @@ type BootstrapOptions = Pick<
|
|
|
550
569
|
* `headScripts: "preinit"`, getClientEntryUrl() (sync — nothing to race)
|
|
551
570
|
* short-circuits to bootstrapModules, and inline content that is exactly
|
|
552
571
|
* `import("<entry-url>")` converts to the URL. React then emits a
|
|
553
|
-
* `<link rel="modulepreload"
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
572
|
+
* `<link rel="modulepreload">` hint in the head plus the executing
|
|
573
|
+
* `<script type="module" src async>` at end of shell — the entry fetch starts
|
|
574
|
+
* with the first flushed bytes instead of when the parser reaches an opaque
|
|
575
|
+
* inline script that only reveals the URL once executed. Fizz writes that hint
|
|
576
|
+
* with `fetchPriority="low"` ahead of the head chunk scripts; the handlers
|
|
577
|
+
* serve it without the attribute after the head chunk scripts
|
|
578
|
+
* (entry-preload-priority.ts, issue #1025), because at Low the entry queued
|
|
579
|
+
* behind in-viewport images and gated hydration (+3.8 s measured on a
|
|
580
|
+
* prioritising HTTP/2 link). Fizz stamps the request nonce on both tags, and
|
|
581
|
+
* under PPR both land in the stored prelude; on resume React has already
|
|
582
|
+
* cleared the bootstrap fields from the postponed state, so nothing re-emits.
|
|
583
|
+
* The conversion is an explicit opt-in:
|
|
560
584
|
* undefined headScripts (a custom SSR entry that predates the option, which
|
|
561
585
|
* also never installed the preinit hook) keeps the inline bootstrap
|
|
562
586
|
* byte-for-byte — converting by default would break CSPs that allowlist the
|
|
@@ -746,8 +770,15 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
746
770
|
await htmlStream.allReady;
|
|
747
771
|
}
|
|
748
772
|
|
|
773
|
+
// The entry's Fizz hint at default priority, after the head chunk
|
|
774
|
+
// scripts (entry-preload-priority.ts).
|
|
775
|
+
const entryUrl = bootstrap.bootstrapModules?.[0];
|
|
776
|
+
const html = entryUrl
|
|
777
|
+
? htmlStream.pipeThrough(entryPreloadPriorityTransform(entryUrl, nonce))
|
|
778
|
+
: htmlStream;
|
|
779
|
+
|
|
749
780
|
// Inject RSC payload into HTML as <script nonce="...">__FLIGHT_DATA__</script>
|
|
750
|
-
return
|
|
781
|
+
return html.pipeThrough(injectRSCPayload(rscStream2, { nonce }));
|
|
751
782
|
} catch (error) {
|
|
752
783
|
reportRenderError(onError, error);
|
|
753
784
|
throw error;
|
|
@@ -952,6 +983,12 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
952
983
|
const result = await prerenderPromise;
|
|
953
984
|
prelude = await readStreamToUint8Array(result.prelude);
|
|
954
985
|
postponed = result.postponed;
|
|
986
|
+
// Same hint rewrite as renderHTML, applied to the stored prelude; a
|
|
987
|
+
// resume never re-emits the bootstrap pair (Fizz clears it).
|
|
988
|
+
const entryUrl = bootstrap.bootstrapModules?.[0];
|
|
989
|
+
if (entryUrl) {
|
|
990
|
+
prelude = rewriteEntryPreloadPriority(prelude, entryUrl);
|
|
991
|
+
}
|
|
955
992
|
} catch (error) {
|
|
956
993
|
// Identity match: swallow ONLY our own deliberate abort
|
|
957
994
|
// (error === abortReason). Not error.name — capture aborts before this
|
|
@@ -81,11 +81,23 @@ export function runWithPreinitNonce<T>(
|
|
|
81
81
|
*
|
|
82
82
|
* Known trades (deliberate, measured neutral-to-positive on the e2e apps —
|
|
83
83
|
* PR #694 has the Lighthouse/hydration numbers):
|
|
84
|
-
* - Fetch priority:
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
84
|
+
* - Fetch priority: Chromium gives a parser-inserted `async` module script
|
|
85
|
+
* initial priority High: below the render-blocking stylesheet (Highest),
|
|
86
|
+
* and alongside the LCP image once Chromium boosts the in-viewport image to
|
|
87
|
+
* High (#1021). Every head chunk gets the same priority: the shared
|
|
88
|
+
* dependency chunks (react, router, entry.rsc and the bundler runtime) and
|
|
89
|
+
* every client-component chunk. The entry itself is a Fizz bootstrapModules
|
|
90
|
+
* hint, not a preinit (Fizz claims its URL, so this hook's call for it is
|
|
91
|
+
* inert); the handlers serve that hint at default priority, High as well,
|
|
92
|
+
* after these scripts (entry-preload-priority.ts, #1025).
|
|
93
|
+
* Lowering the chunks to `fetchpriority="low"` was measured and rejected
|
|
94
|
+
* (#1021; Chromium 153, 20 interleaved runs per variant and cell). On pages
|
|
95
|
+
* with images above the fold the Low chunks queue behind the boosted
|
|
96
|
+
* images: hydration came 206 to 6407 ms later on HTTP/1.1 and 65 to 642 ms
|
|
97
|
+
* later on HTTP/2. The one gain, FCP -136 ms, needed an image-free page
|
|
98
|
+
* with 35 KB of render-blocking CSS on HTTP/1.1 Slow 4G, and hydration was
|
|
99
|
+
* still 98 ms later there. `headScripts: "preload"` is the way to get Low
|
|
100
|
+
* chunk fetches.
|
|
89
101
|
* - Build only: plugin-rsc's dev load path reports `js: []` per reference, so
|
|
90
102
|
* dev documents have no head chunk scripts — a client module whose module
|
|
91
103
|
* scope assumes body-parsed DOM can break in production only. The
|
package/src/vite/plugin-types.ts
CHANGED
|
@@ -83,6 +83,12 @@ export interface ClientChunkMeta {
|
|
|
83
83
|
* strategy**. It splits app `"use client"` modules by **route id** — the segment
|
|
84
84
|
* after a route-root directory (`routes`, `app`, `pages`, `features`, `handlers`,
|
|
85
85
|
* …) — so `routes/dashboard/**` becomes `app-dashboard` at any nesting depth.
|
|
86
|
+
* The first marker in the path wins, except that an `app/` directly followed
|
|
87
|
+
* by another marker with a directory after it is treated as a source root:
|
|
88
|
+
* `app/routes/product/**` becomes `app-product`, while `app/dashboard/**`
|
|
89
|
+
* (Next-style), `app/components/**` and files directly in `app/routes/` stay
|
|
90
|
+
* `app-dashboard` / `app-components` / `app-routes`. The same route id under
|
|
91
|
+
* different markers is one group.
|
|
86
92
|
* Where it finds NO route structure (a flat `src/components/`, or host sub-apps
|
|
87
93
|
* already split by a dynamic `import()`), it inherits the default grouping
|
|
88
94
|
* unchanged — so the shared `src/components` chunk stays shared and host apps do
|
|
@@ -94,10 +100,14 @@ export interface ClientChunkMeta {
|
|
|
94
100
|
* back to the default grouping for that one module. Forwarded directly to
|
|
95
101
|
* `@vitejs/plugin-rsc`'s `clientChunks`.
|
|
96
102
|
*
|
|
97
|
-
* Every module maps to exactly one group, so there is no byte duplication
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
103
|
+
* Every module maps to exactly one group, so there is no byte duplication. A
|
|
104
|
+
* group is also the loading unit: rendering any one member downloads the whole
|
|
105
|
+
* group's chunk, every other member included. A component used by two routes
|
|
106
|
+
* lives in one group, and rendering it on either route fetches that entire
|
|
107
|
+
* group. Put genuinely shared client components OUTSIDE route directories so
|
|
108
|
+
* they land in the shared group rather than one route's chunk, and keep an
|
|
109
|
+
* always-rendered component (a layout header) out of a large group.
|
|
110
|
+
* `DEBUG=rango:chunks vite build` logs each group's module count and size.
|
|
101
111
|
*
|
|
102
112
|
* @default true
|
|
103
113
|
*/
|
|
@@ -137,14 +147,18 @@ interface RangoBaseOptions {
|
|
|
137
147
|
* - `"preinit"` (**default**): client-reference chunks render as EXECUTING
|
|
138
148
|
* `<script type="module" async>` tags hoisted into `<head>` (upgrading
|
|
139
149
|
* plugin-rsc's modulepreload hints in place), and the browser entry ships
|
|
140
|
-
* as Fizz `bootstrapModules` — a
|
|
141
|
-
*
|
|
150
|
+
* as Fizz `bootstrapModules` — a `modulepreload` hint after the head
|
|
151
|
+
* chunk scripts (default priority; Fizz's `fetchpriority=low` is dropped,
|
|
152
|
+
* #1025)
|
|
153
|
+
* plus the executing end-of-shell `id="_R_"` module script. Chunk
|
|
142
154
|
* execution overlaps body streaming instead of waiting for the hydration
|
|
143
155
|
* import walk; under PPR everything lands in the stored shell prelude.
|
|
144
156
|
* - `"preload"`: the previous behavior — `<link rel="modulepreload">` hints
|
|
145
157
|
* only, entry as an inline `import()` script at end of shell. Chunks
|
|
146
158
|
* fetch+compile early but execute only when hydration imports them.
|
|
147
159
|
*
|
|
160
|
+
* Any other value throws when the generated SSR entry is built.
|
|
161
|
+
*
|
|
148
162
|
* Build-only for the chunk half: plugin-rsc resolves no JS deps per client
|
|
149
163
|
* reference in dev, so dev documents carry no head chunk scripts in either
|
|
150
164
|
* mode (the bootstrap conversion does apply in dev). Trades and upstream
|
|
@@ -65,13 +65,20 @@ function emitProgressiveChunkSize(value: number): string {
|
|
|
65
65
|
* option: "preinit" (default) installs the client-reference preinit hook and
|
|
66
66
|
* threads `getClientEntryUrl` so Fizz emits `bootstrapModules`;
|
|
67
67
|
* "preload" omits the hook and uses the deprecated inline
|
|
68
|
-
* `loadBootstrapScriptContent` bootstrap.
|
|
68
|
+
* `loadBootstrapScriptContent` bootstrap. Any other value throws: an unknown
|
|
69
|
+
* string such as "prenit" used to run as "preinit" silently
|
|
70
|
+
* (`headScripts !== "preload"` since #694).
|
|
69
71
|
*/
|
|
70
72
|
export function getVirtualEntrySSR(
|
|
71
73
|
headScripts: HeadScriptsOption = "preinit",
|
|
72
74
|
progressiveChunkSize?: number,
|
|
73
75
|
): string {
|
|
74
|
-
|
|
76
|
+
if (headScripts !== "preinit" && headScripts !== "preload") {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`rango({ headScripts }) must be "preinit" or "preload", received ${JSON.stringify(headScripts)}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const preinit = headScripts === "preinit";
|
|
75
82
|
// The preload variant drops the preinit-only imports/install and swaps the
|
|
76
83
|
// bootstrap dep, all built here so the template below stays a single
|
|
77
84
|
// unconditional shape.
|
package/src/vite/rango.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
getManualChunks,
|
|
33
33
|
} from "./utils/shared-utils.js";
|
|
34
34
|
import {
|
|
35
|
+
clientChunksReport,
|
|
35
36
|
resolveClientChunks,
|
|
36
37
|
type ClientChunkContext,
|
|
37
38
|
} from "./utils/client-chunks.js";
|
|
@@ -537,6 +538,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
537
538
|
);
|
|
538
539
|
plugins.push(clientRefDedup());
|
|
539
540
|
}
|
|
541
|
+
plugins.push(clientChunksReport());
|
|
540
542
|
|
|
541
543
|
plugins.push({
|
|
542
544
|
name: "@rangojs/router:client-component-hmr",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { gzipSync } from "node:zlib";
|
|
1
3
|
import type { ClientChunkMeta, ClientChunks } from "../plugin-types.js";
|
|
2
|
-
import { createRangoDebugger, NS } from "../debug.js";
|
|
4
|
+
import { createRangoDebugger, NS, type Debugger } from "../debug.js";
|
|
3
5
|
import { hashRefKey } from "../plugins/client-ref-hashing.js";
|
|
4
6
|
|
|
5
7
|
/** The callback shape @vitejs/plugin-rsc's `clientChunks` option accepts. */
|
|
@@ -97,6 +99,20 @@ const ROUTE_ROOT_DIRS = new Set([
|
|
|
97
99
|
"sections",
|
|
98
100
|
]);
|
|
99
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Markers that double as a conventional source root. `app/` is both a Next-style
|
|
104
|
+
* route root (`app/<segment>/…`) and the source root of many apps
|
|
105
|
+
* (`app/components`, `app/routes/<id>`). Keying on the segment after it put every
|
|
106
|
+
* route under `app/routes/` into one `app-routes` group, and a group is the
|
|
107
|
+
* loading unit, so rendering any one member downloaded every route's client code
|
|
108
|
+
* (#1022). A source-root marker therefore defers to a marker that IMMEDIATELY
|
|
109
|
+
* follows it when that inner marker has a route directory after it
|
|
110
|
+
* (`app/routes/<id>/…` -> `app-<id>`). Anything else after `app/` (including
|
|
111
|
+
* `app/components/`) is still the Next-style route id. Only these markers defer:
|
|
112
|
+
* `routes/view/<sub>/…` keeps `view` as the route id.
|
|
113
|
+
*/
|
|
114
|
+
const SOURCE_ROOT_DIRS = new Set(["app"]);
|
|
115
|
+
|
|
100
116
|
/**
|
|
101
117
|
* Built-in strategy used when `clientChunks: true` (also the default). Splits app
|
|
102
118
|
* client components by route/feature identity ONLY where it can recognize a route
|
|
@@ -116,8 +132,11 @@ const ROUTE_ROOT_DIRS = new Set([
|
|
|
116
132
|
* 1. Shared runtime (React / router / node_modules) -> `undefined` (never split).
|
|
117
133
|
* 2. A registered error/notFound fallback (`ctx.fallbackRefs`) -> `app-fallback`,
|
|
118
134
|
* regardless of location, so the error UI is decoupled from the happy path.
|
|
119
|
-
* 3.
|
|
120
|
-
* next segment (the route id), robust to any nesting depth.
|
|
135
|
+
* 3. The first {@link ROUTE_ROOT_DIRS} marker with a directory after it -> key on
|
|
136
|
+
* that next segment (the route id), robust to any nesting depth. A
|
|
137
|
+
* {@link SOURCE_ROOT_DIRS} marker (`app`) directly followed by another marker
|
|
138
|
+
* with a directory after it defers to that inner marker:
|
|
139
|
+
* `app/routes/<id>/…` -> `app-<id>`, while `app/<segment>/…` -> `app-<segment>`.
|
|
121
140
|
* 4. Otherwise `undefined` (inherit the default `serverChunk` grouping).
|
|
122
141
|
*/
|
|
123
142
|
export function directoryClientChunks(
|
|
@@ -140,17 +159,21 @@ export function directoryClientChunks(
|
|
|
140
159
|
}
|
|
141
160
|
const segments = meta.normalizedId.split("/").filter(Boolean);
|
|
142
161
|
const dirCount = segments.length - 1; // exclude the filename
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
162
|
+
// Route-root marker -> the segment after it is the route id. First marker wins,
|
|
163
|
+
// so a top-level route owns its whole subtree. The `< dirCount - 1` bound
|
|
164
|
+
// guarantees the segment after the marker is a directory, not the file.
|
|
165
|
+
const isMarkerAt = (i: number) =>
|
|
166
|
+
i < dirCount - 1 && ROUTE_ROOT_DIRS.has(segments[i].toLowerCase());
|
|
167
|
+
for (let i = 0; i < dirCount - 1; i++) {
|
|
168
|
+
if (!isMarkerAt(i)) continue;
|
|
169
|
+
// A source root (`app/`) yields to an inner marker right after it.
|
|
170
|
+
const marker =
|
|
171
|
+
SOURCE_ROOT_DIRS.has(segments[i].toLowerCase()) && isMarkerAt(i + 1)
|
|
172
|
+
? i + 1
|
|
173
|
+
: i;
|
|
174
|
+
const group = `app-${sanitizeGroup(segments[marker + 1])}`;
|
|
175
|
+
debugChunks?.("split %s -> %s", meta.normalizedId, group);
|
|
176
|
+
return group;
|
|
154
177
|
}
|
|
155
178
|
// No recognized route structure -> inherit the default serverChunk grouping.
|
|
156
179
|
// This is the actionable "silent" case: app code that did NOT split by route.
|
|
@@ -182,3 +205,46 @@ export function resolveClientChunks(
|
|
|
182
205
|
if (option === true) return (meta) => directoryClientChunks(meta, ctx);
|
|
183
206
|
return option;
|
|
184
207
|
}
|
|
208
|
+
|
|
209
|
+
/** Module-id prefix of the per-group virtual module @vitejs/plugin-rsc emits. */
|
|
210
|
+
const CLIENT_GROUP_PREFIX = "\0virtual:vite-rsc/client-references/group/";
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Build-end group report under `DEBUG=rango:chunks`: one line per client
|
|
214
|
+
* reference group with its client-reference count and the size of the group's
|
|
215
|
+
* own chunk (code split into shared chunks such as `react`/`router` is not
|
|
216
|
+
* counted). A group is the loading unit (plugin-rsc imports the group's virtual
|
|
217
|
+
* module to resolve any one member), so a large group that an always-rendered
|
|
218
|
+
* component belongs to is downloaded on every route; this line makes that
|
|
219
|
+
* visible. Covers every group, including ones named by a custom `clientChunks`
|
|
220
|
+
* function or by plugin-rsc's default `serverChunk` grouping. `undefined` when
|
|
221
|
+
* the namespace is off, so a normal build does not register it. Group names are
|
|
222
|
+
* printed with NUL escaped: plugin-rsc's default group is named
|
|
223
|
+
* `facade:\0virtual:…`, and a raw NUL makes `grep` treat the output as binary
|
|
224
|
+
* and drop that line, usually the largest group. `log` is injectable for tests.
|
|
225
|
+
*/
|
|
226
|
+
export function clientChunksReport(
|
|
227
|
+
log: Debugger | undefined = debugChunks,
|
|
228
|
+
): Plugin | undefined {
|
|
229
|
+
if (!log) return undefined;
|
|
230
|
+
return {
|
|
231
|
+
name: "@rangojs/router:client-chunks-report",
|
|
232
|
+
apply: "build",
|
|
233
|
+
applyToEnvironment: (env) => env.name === "client",
|
|
234
|
+
generateBundle(_options, bundle) {
|
|
235
|
+
for (const chunk of Object.values(bundle)) {
|
|
236
|
+
if (chunk.type !== "chunk") continue;
|
|
237
|
+
const facade = chunk.facadeModuleId;
|
|
238
|
+
if (!facade?.startsWith(CLIENT_GROUP_PREFIX)) continue;
|
|
239
|
+
log(
|
|
240
|
+
"group %s: %d client reference(s), %d B (%d B gzip) -> %s",
|
|
241
|
+
facade.slice(CLIENT_GROUP_PREFIX.length).replaceAll("\0", "\\0"),
|
|
242
|
+
this.getModuleInfo(facade)?.importedIds.length ?? 0,
|
|
243
|
+
Buffer.byteLength(chunk.code),
|
|
244
|
+
gzipSync(chunk.code).length,
|
|
245
|
+
chunk.fileName,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|