@rangojs/router 0.0.0-experimental.113 → 0.0.0-experimental.115
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 +73 -2
- package/dist/vite/index.js +193 -19
- package/package.json +18 -17
- package/skills/hooks/SKILL.md +3 -3
- package/skills/links/SKILL.md +10 -10
- package/skills/rango/SKILL.md +1 -0
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/view-transitions/SKILL.md +85 -3
- package/src/browser/react/use-reverse.ts +19 -12
- package/src/build/route-types/router-processing.ts +14 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/handle.ts +3 -5
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +2 -5
- package/src/missing-id-error.ts +68 -0
- package/src/reverse.ts +16 -13
- package/src/route-definition/dsl-helpers.ts +5 -2
- package/src/route-definition/helpers-types.ts +31 -19
- package/src/router/router-options.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +17 -4
- package/src/router/segment-resolution/revalidation.ts +17 -4
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +2 -0
- package/src/segment-system.tsx +18 -2
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +9 -1
- package/src/vite/debug.ts +1 -0
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +68 -12
- package/src/vite/plugins/expose-internal-ids.ts +12 -4
- package/src/vite/rango.ts +12 -0
- package/src/vite/router-discovery.ts +14 -2
- package/src/vite/utils/client-chunks.ts +156 -0
- package/src/vite/utils/shared-utils.ts +10 -3
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
getImportedFnNames,
|
|
29
29
|
collectCreateExportBindings,
|
|
30
30
|
buildUnsupportedShapeWarning,
|
|
31
|
+
findUnsupportedCreateCallSites,
|
|
31
32
|
isExportOnlyFile,
|
|
32
33
|
} from "./expose-ids/export-analysis.js";
|
|
33
34
|
import {
|
|
@@ -370,14 +371,21 @@ ${lazyImports.join(",\n")}
|
|
|
370
371
|
if (!hasCode) continue;
|
|
371
372
|
|
|
372
373
|
const fnNames = getFnNames(cfg.fnName);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
374
|
+
// Locate the real (comment/string-free) create* calls not covered by
|
|
375
|
+
// a supported, id-injectable export shape. Empty means every call is
|
|
376
|
+
// fine — in particular, a create*() token in a comment or string no
|
|
377
|
+
// longer trips a spurious warning.
|
|
378
|
+
const sites = findUnsupportedCreateCallSites(
|
|
379
|
+
code,
|
|
380
|
+
fnNames,
|
|
381
|
+
getBindings(code, fnNames),
|
|
382
|
+
);
|
|
383
|
+
if (sites.length === 0) continue;
|
|
376
384
|
|
|
377
385
|
const warnKey = `${id}::${cfg.fnName}`;
|
|
378
386
|
if (unsupportedShapeWarnings.has(warnKey)) continue;
|
|
379
387
|
unsupportedShapeWarnings.add(warnKey);
|
|
380
|
-
this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName));
|
|
388
|
+
this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName, sites));
|
|
381
389
|
}
|
|
382
390
|
|
|
383
391
|
// --- Loader: track for manifest (RSC env only) ---
|
package/src/vite/rango.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
onwarn,
|
|
24
24
|
getManualChunks,
|
|
25
25
|
} from "./utils/shared-utils.js";
|
|
26
|
+
import { resolveClientChunks } from "./utils/client-chunks.js";
|
|
26
27
|
import type { RangoOptions } from "./plugin-types.js";
|
|
27
28
|
import { printBanner, rangoVersion } from "./utils/banner.js";
|
|
28
29
|
import { createVersionInjectorPlugin } from "./plugins/version-injector.js";
|
|
@@ -62,6 +63,15 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
62
63
|
const resolvedOptions: RangoOptions = options ?? { preset: "node" };
|
|
63
64
|
const preset = resolvedOptions.preset ?? "node";
|
|
64
65
|
const showBanner = resolvedOptions.banner ?? true;
|
|
66
|
+
// Client-chunking strategy (per-route/per-feature splitting of the browser
|
|
67
|
+
// bundle). Defaults to the built-in directory strategy (`true`) pre-1.0; pass
|
|
68
|
+
// `clientChunks: false` to opt out. Resolved once and forwarded to
|
|
69
|
+
// @vitejs/plugin-rsc in both presets. The built-in strategy only splits where it
|
|
70
|
+
// recognizes a route structure, so this default is a no-op for flat / host-split
|
|
71
|
+
// apps and never duplicates the shared runtime.
|
|
72
|
+
const clientChunks = resolveClientChunks(
|
|
73
|
+
resolvedOptions.clientChunks ?? true,
|
|
74
|
+
);
|
|
65
75
|
debugConfig?.("rango(%s) setup start", preset);
|
|
66
76
|
|
|
67
77
|
const plugins: PluginOption[] = [];
|
|
@@ -231,6 +241,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
231
241
|
rsc({
|
|
232
242
|
entries: finalEntries,
|
|
233
243
|
serverHandler: false,
|
|
244
|
+
clientChunks,
|
|
234
245
|
}) as PluginOption,
|
|
235
246
|
);
|
|
236
247
|
|
|
@@ -394,6 +405,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
394
405
|
plugins.push(
|
|
395
406
|
rsc({
|
|
396
407
|
entries: finalEntries,
|
|
408
|
+
clientChunks,
|
|
397
409
|
}) as PluginOption,
|
|
398
410
|
);
|
|
399
411
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
findNestedRouterConflict,
|
|
18
18
|
findRouterFiles,
|
|
19
19
|
} from "../build/generate-route-types.js";
|
|
20
|
+
import { firstCodeMatchIndex } from "../build/route-types/source-scan.js";
|
|
20
21
|
import { createVersionPlugin } from "./plugins/version-plugin.js";
|
|
21
22
|
import { createVirtualStubPlugin } from "./plugins/virtual-stub-plugin.js";
|
|
22
23
|
import {
|
|
@@ -1184,8 +1185,19 @@ export function createRouterDiscoveryPlugin(
|
|
|
1184
1185
|
trimmed.startsWith('"use client"') ||
|
|
1185
1186
|
trimmed.startsWith("'use client'");
|
|
1186
1187
|
if (!inRecoveryMode && isUseClient) return;
|
|
1187
|
-
|
|
1188
|
-
|
|
1188
|
+
// Cheap raw pre-check first; only when a candidate token is present
|
|
1189
|
+
// do we confirm it occurs in real code (not a comment/string) via a
|
|
1190
|
+
// single allocation-free code-region scan. Most saved files contain
|
|
1191
|
+
// neither token and skip the scan entirely. This avoids a comment or
|
|
1192
|
+
// string mention spuriously marking a file relevant and triggering an
|
|
1193
|
+
// unnecessary re-discovery on save.
|
|
1194
|
+
let hasUrls = source.includes("urls(");
|
|
1195
|
+
let hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
|
|
1196
|
+
if (hasUrls) hasUrls = firstCodeMatchIndex(source, /urls\(/g) >= 0;
|
|
1197
|
+
if (hasCreateRouter) {
|
|
1198
|
+
hasCreateRouter =
|
|
1199
|
+
firstCodeMatchIndex(source, /\bcreateRouter\s*[<(]/g) >= 0;
|
|
1200
|
+
}
|
|
1189
1201
|
if (!inRecoveryMode && !hasUrls && !hasCreateRouter) return;
|
|
1190
1202
|
if (inRecoveryMode) {
|
|
1191
1203
|
debugDiscovery?.(
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Resolution of the public `clientChunks` option into the callback shape that
|
|
2
|
+
// @vitejs/plugin-rsc expects. See plugin-types.ts (ClientChunks) and
|
|
3
|
+
// docs/client-chunking.md for the contract. The mechanism: a distinct returned
|
|
4
|
+
// name yields a distinct, dynamically-imported client chunk, independent of how
|
|
5
|
+
// the RSC/server build chunked the importing modules.
|
|
6
|
+
|
|
7
|
+
import type { ClientChunkMeta, ClientChunks } from "../plugin-types.js";
|
|
8
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
9
|
+
|
|
10
|
+
/** The callback shape @vitejs/plugin-rsc's `clientChunks` option accepts. */
|
|
11
|
+
export type RscClientChunksFn = (meta: ClientChunkMeta) => string | undefined;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Opt-in observability for the built-in strategy. The route-root marker list is
|
|
15
|
+
* intentionally finite (see {@link ROUTE_ROOT_DIRS}); a consumer whose layout
|
|
16
|
+
* has no recognized marker (e.g. `src/parts/<feature>/…`) silently inherits the
|
|
17
|
+
* default grouping (no per-route split). That silence is the only real downside
|
|
18
|
+
* of a convention-based default, so we make the decision observable: run a build
|
|
19
|
+
* with `DEBUG=rango:chunks` to see, per client module, which route group it was
|
|
20
|
+
* assigned to or why it fell back to the shared grouping. Zero cost when off
|
|
21
|
+
* (the debugger is `undefined` unless the namespace is enabled). For full control
|
|
22
|
+
* over any layout, pass a `clientChunks` function instead of relying on the
|
|
23
|
+
* convention — that is the supported configurability path, not widening the list.
|
|
24
|
+
*/
|
|
25
|
+
const debugChunks = createRangoDebugger(NS.chunks);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Modules that must stay on the default (shared) grouping regardless of strategy:
|
|
29
|
+
* React, the router client runtime, and anything in node_modules. Splitting these
|
|
30
|
+
* out per route would fragment the shared baseline and regress cache reuse — they
|
|
31
|
+
* are loaded on every route, so they belong in shared chunks.
|
|
32
|
+
*
|
|
33
|
+
* The Rango runtime is matched by package root only: `@rangojs/router` (the
|
|
34
|
+
* installed/aliased name) and the workspace `packages/(rangojs-router|rsc-router)/(src|dist)/`.
|
|
35
|
+
* The `(src|dist)` anchor matches the package's own source/build output but NOT
|
|
36
|
+
* consumer apps that merely live under a `packages/rangojs-router/` ancestor (the
|
|
37
|
+
* in-repo e2e apps), so their app components remain splittable. We deliberately do
|
|
38
|
+
* NOT match a bare `/src/browser/`: that is a consumer-owned path (a consumer's own
|
|
39
|
+
* `src/browser/Foo.tsx` must still split).
|
|
40
|
+
*
|
|
41
|
+
* We test BOTH `meta.id` (absolute) and `meta.normalizedId`. `normalizedId` is the
|
|
42
|
+
* project-root-relative form plugin-rsc derives (e.g. `../../src/browser/react/Link.tsx`
|
|
43
|
+
* for the in-repo runtime), which the package-root patterns miss; the absolute `id`
|
|
44
|
+
* always contains the package's real location, so it reliably catches the runtime.
|
|
45
|
+
*/
|
|
46
|
+
function isSharedRuntime(meta: ClientChunkMeta): boolean {
|
|
47
|
+
return [meta.id, meta.normalizedId].some(
|
|
48
|
+
(path) =>
|
|
49
|
+
path.includes("/node_modules/") ||
|
|
50
|
+
/\/@rangojs\/router\//.test(path) ||
|
|
51
|
+
/\/packages\/(rangojs-router|rsc-router)\/(src|dist)\//.test(path),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Sanitize a raw group name into a filesystem/Rollup-safe chunk name fragment. */
|
|
56
|
+
function sanitizeGroup(name: string): string {
|
|
57
|
+
return name.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "") || "app";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Directory names that conventionally hold one sub-directory per route/feature.
|
|
62
|
+
* When a `"use client"` module lives under one of these, the built-in strategy
|
|
63
|
+
* keys the chunk on the segment IMMEDIATELY AFTER the marker (the route id),
|
|
64
|
+
* rather than the module's immediate parent directory. This is what keeps
|
|
65
|
+
* `routes/foo/components/Button.tsx` and `routes/bar/components/Button.tsx` in
|
|
66
|
+
* `app-foo` / `app-bar` instead of colliding in a single `app-components`.
|
|
67
|
+
*
|
|
68
|
+
* Route identity lives in the path PREFIX; the immediate parent (a suffix) is
|
|
69
|
+
* only a reliable proxy for the un-nested `routes/<route>/Widget.tsx` layout.
|
|
70
|
+
*/
|
|
71
|
+
const ROUTE_ROOT_DIRS = new Set([
|
|
72
|
+
"routes",
|
|
73
|
+
"route",
|
|
74
|
+
"pages",
|
|
75
|
+
"page",
|
|
76
|
+
"app",
|
|
77
|
+
"features",
|
|
78
|
+
"feature",
|
|
79
|
+
"views",
|
|
80
|
+
"view",
|
|
81
|
+
"handlers",
|
|
82
|
+
"urls",
|
|
83
|
+
"modules",
|
|
84
|
+
"screens",
|
|
85
|
+
"sections",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Built-in strategy used when `clientChunks: true` (also the default). Splits app
|
|
90
|
+
* client components by route/feature identity ONLY where it can recognize a route
|
|
91
|
+
* structure; everywhere else it inherits the default grouping (returns undefined).
|
|
92
|
+
* This conservatism is what makes it safe as a default:
|
|
93
|
+
*
|
|
94
|
+
* - A recognized route structure (`routes/<id>/…`, `app/<id>/…`, `handlers/<id>/…`
|
|
95
|
+
* etc.) splits into a per-route chunk `app-<id>`, at any nesting depth.
|
|
96
|
+
* - A flat `src/components/Button.tsx`, or host sub-apps already split by a dynamic
|
|
97
|
+
* `import()` boundary (each app's `serverChunk` differs), get `undefined` and so
|
|
98
|
+
* keep `@vitejs/plugin-rsc`'s default `serverChunk` grouping — i.e. NO change
|
|
99
|
+
* versus not enabling the option. Returning a parent-dir name here would instead
|
|
100
|
+
* merge unrelated modules (e.g. every host app's `components/Layout.tsx` into one
|
|
101
|
+
* `app-components`), re-introducing cross-app leakage.
|
|
102
|
+
*
|
|
103
|
+
* Resolution order:
|
|
104
|
+
* 1. If the path passes through a {@link ROUTE_ROOT_DIRS} marker that has a
|
|
105
|
+
* directory after it, key on that next segment (the route id) — robust to any
|
|
106
|
+
* nesting depth below it (`routes/foo/components/ui/X.tsx` -> `app-foo`).
|
|
107
|
+
* 2. Otherwise return `undefined` (inherit the default `serverChunk` grouping).
|
|
108
|
+
*/
|
|
109
|
+
export function directoryClientChunks(
|
|
110
|
+
meta: ClientChunkMeta,
|
|
111
|
+
): string | undefined {
|
|
112
|
+
if (isSharedRuntime(meta)) {
|
|
113
|
+
// React / router runtime / node_modules: always shared, expected, uninteresting.
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const segments = meta.normalizedId.split("/").filter(Boolean);
|
|
117
|
+
const dirCount = segments.length - 1; // exclude the filename
|
|
118
|
+
if (dirCount >= 1) {
|
|
119
|
+
// Route-root marker -> the segment after it is the route id. First marker
|
|
120
|
+
// wins, so a top-level route owns its whole subtree. The `< dirCount - 1`
|
|
121
|
+
// bound guarantees the segment after the marker is a directory, not the file.
|
|
122
|
+
for (let i = 0; i < dirCount - 1; i++) {
|
|
123
|
+
if (ROUTE_ROOT_DIRS.has(segments[i].toLowerCase())) {
|
|
124
|
+
const group = `app-${sanitizeGroup(segments[i + 1])}`;
|
|
125
|
+
debugChunks?.("split %s -> %s", meta.normalizedId, group);
|
|
126
|
+
return group;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// No recognized route structure -> inherit the default serverChunk grouping.
|
|
131
|
+
// This is the actionable "silent" case: app code that did NOT split by route.
|
|
132
|
+
// Surface it (under DEBUG=rango:chunks) so a consumer can see their layout
|
|
133
|
+
// missed the convention and either colocate under a marker dir or pass a fn.
|
|
134
|
+
debugChunks?.(
|
|
135
|
+
"shared %s (no route-root marker; inherits default grouping)",
|
|
136
|
+
meta.normalizedId,
|
|
137
|
+
);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve a Rango `clientChunks` option into a @vitejs/plugin-rsc `clientChunks`
|
|
143
|
+
* callback, or `undefined` to leave plugin-rsc on its default (serverChunk)
|
|
144
|
+
* grouping.
|
|
145
|
+
*
|
|
146
|
+
* - `false` / `undefined` -> `undefined` (no override).
|
|
147
|
+
* - `true` -> the built-in {@link directoryClientChunks} strategy.
|
|
148
|
+
* - function -> the user's function, used verbatim.
|
|
149
|
+
*/
|
|
150
|
+
export function resolveClientChunks(
|
|
151
|
+
option: ClientChunks | undefined,
|
|
152
|
+
): RscClientChunksFn | undefined {
|
|
153
|
+
if (!option) return undefined;
|
|
154
|
+
if (option === true) return directoryClientChunks;
|
|
155
|
+
return option;
|
|
156
|
+
}
|
|
@@ -173,12 +173,19 @@ export function getManualChunks(id: string): string | undefined {
|
|
|
173
173
|
return "react";
|
|
174
174
|
}
|
|
175
175
|
// Use dynamic package name from package.json
|
|
176
|
-
// Check both npm install path and workspace symlink resolved path
|
|
176
|
+
// Check both npm install path and workspace symlink resolved path.
|
|
177
|
+
//
|
|
178
|
+
// The workspace patterns are anchored to the package's own `src`/`dist` so
|
|
179
|
+
// they match the router runtime but NOT consumer apps that merely live under a
|
|
180
|
+
// `packages/rangojs-router/` ancestor (the in-repo e2e apps at
|
|
181
|
+
// `packages/rangojs-router/e2e/<app>/src/...`). Without the anchor those apps'
|
|
182
|
+
// own client components were force-merged into the shared "router" chunk,
|
|
183
|
+
// which both misrepresented real-consumer bundles and blocked `clientChunks`
|
|
184
|
+
// splitting from relocating them.
|
|
177
185
|
const packageName = getPublishedPackageName();
|
|
178
186
|
if (
|
|
179
187
|
normalized.includes(`node_modules/${packageName}/`) ||
|
|
180
|
-
|
|
181
|
-
normalized.includes("packages/rangojs-router/")
|
|
188
|
+
/\/packages\/(rsc-router|rangojs-router)\/(src|dist)\//.test(normalized)
|
|
182
189
|
) {
|
|
183
190
|
return "router";
|
|
184
191
|
}
|