@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.
Files changed (36) hide show
  1. package/dist/bin/rango.js +73 -2
  2. package/dist/vite/index.js +193 -19
  3. package/package.json +18 -17
  4. package/skills/hooks/SKILL.md +3 -3
  5. package/skills/links/SKILL.md +10 -10
  6. package/skills/rango/SKILL.md +1 -0
  7. package/skills/react-compiler/SKILL.md +168 -0
  8. package/skills/view-transitions/SKILL.md +85 -3
  9. package/src/browser/react/use-reverse.ts +19 -12
  10. package/src/build/route-types/router-processing.ts +14 -1
  11. package/src/build/route-types/source-scan.ts +118 -0
  12. package/src/handle.ts +3 -5
  13. package/src/loader.rsc.ts +2 -5
  14. package/src/loader.ts +2 -5
  15. package/src/missing-id-error.ts +68 -0
  16. package/src/reverse.ts +16 -13
  17. package/src/route-definition/dsl-helpers.ts +5 -2
  18. package/src/route-definition/helpers-types.ts +31 -19
  19. package/src/router/router-options.ts +24 -0
  20. package/src/router/segment-resolution/fresh.ts +17 -4
  21. package/src/router/segment-resolution/revalidation.ts +17 -4
  22. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  23. package/src/router/types.ts +8 -0
  24. package/src/router.ts +2 -0
  25. package/src/segment-system.tsx +18 -2
  26. package/src/types/segments.ts +18 -1
  27. package/src/urls/path-helper-types.ts +9 -1
  28. package/src/vite/debug.ts +1 -0
  29. package/src/vite/index.ts +2 -0
  30. package/src/vite/plugin-types.ts +67 -0
  31. package/src/vite/plugins/expose-ids/export-analysis.ts +68 -12
  32. package/src/vite/plugins/expose-internal-ids.ts +12 -4
  33. package/src/vite/rango.ts +12 -0
  34. package/src/vite/router-discovery.ts +14 -2
  35. package/src/vite/utils/client-chunks.ts +156 -0
  36. 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
- const totalCalls = countCreateCallsForNames(code, fnNames);
374
- const supportedBindings = getBindings(code, fnNames).length;
375
- if (totalCalls <= supportedBindings) continue;
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
- const hasUrls = source.includes("urls(");
1188
- const hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
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
- normalized.includes("packages/rsc-router/") ||
181
- normalized.includes("packages/rangojs-router/")
188
+ /\/packages\/(rsc-router|rangojs-router)\/(src|dist)\//.test(normalized)
182
189
  ) {
183
190
  return "router";
184
191
  }