@rangojs/router 0.30.0 → 0.31.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/bin/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/dist/bin/rango.js +7861 -197
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -7
- 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/types/vite/temp-rsc-server.d.ts +8 -4
- package/dist/types/vite/utils/forward-user-plugins.d.ts +3 -1
- package/dist/vite/index.js +153 -50
- package/package.json +2 -2
- package/skills/rango/SKILL.md +5 -5
- package/skills/testing/response-routes.md +3 -2
- package/skills/typesafety/generated-files-and-cli.md +2 -1
- package/src/bin/rango.ts +13 -8
- package/src/build/generate-route-types.ts +0 -1
- package/src/build/route-types/include-resolution.ts +2 -1
- package/src/build/route-types/router-processing.ts +213 -49
- package/src/build/runtime-discovery.ts +90 -41
- package/src/client-urls/server-projection.ts +3 -1
- 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/src/vite/temp-rsc-server.ts +9 -4
- package/src/vite/utils/forward-user-plugins.ts +4 -2
|
@@ -4,5 +4,5 @@ export { generatePerModuleTypesSource, generateRouteTypesSource, } from "./route
|
|
|
4
4
|
export { DEFAULT_EXCLUDE_PATTERNS, type ScanFilter, createScanFilter, findTsFiles, isInSkippedScanDir, } from "./route-types/scan-filter.js";
|
|
5
5
|
export { writePerModuleRouteTypes, writePerModuleRouteTypesForFile, } from "./route-types/per-module-writer.js";
|
|
6
6
|
export { type UnresolvableReason, type UnresolvableInclude, extractIncludesWithDiagnostics, } from "./route-types/include-resolution.js";
|
|
7
|
-
export { extractUrlsFromRouter,
|
|
7
|
+
export { extractUrlsFromRouter, type UrlsExtractionResult, buildCombinedRouteMapForRouterFile, detectUnresolvableIncludes, detectUnresolvableIncludesForUrlsFile, findNestedRouterConflict, formatNestedRouterConflictError, findRouterFiles, findHostRouterFiles, writeCombinedRouteTypes, genFileTsPath, resolveSearchSchemas, } from "./route-types/router-processing.js";
|
|
8
8
|
export { findUrlsVariableNames } from "./route-types/per-module-writer.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
export type UnresolvableReason = "factory-call" | "dynamic-expression" | "unresolvable-import" | "file-not-found";
|
|
2
|
+
export type UnresolvableReason = "factory-call" | "dynamic-expression" | "unresolvable-import" | "file-not-found" | "unresolvable-basename";
|
|
3
3
|
export interface UnresolvableInclude {
|
|
4
4
|
pathPrefix: string;
|
|
5
5
|
namePrefix: string | null;
|
|
@@ -30,11 +30,6 @@ export type UrlsExtractionResult = {
|
|
|
30
30
|
* Returns either a variable name or an inline code block.
|
|
31
31
|
*/
|
|
32
32
|
export declare function extractUrlsFromRouter(code: string): UrlsExtractionResult | null;
|
|
33
|
-
/**
|
|
34
|
-
* Extract the `basename` string literal from createRouter({ basename: "..." }).
|
|
35
|
-
* Returns the basename value or undefined if not present.
|
|
36
|
-
*/
|
|
37
|
-
export declare function extractBasenameFromRouter(code: string): string | undefined;
|
|
38
33
|
export declare function genFileTsPath(sourceFile: string): string;
|
|
39
34
|
export declare function resolveSearchSchemas(publicRouteNames: string[], runtimeSchemas: Record<string, Record<string, string>> | undefined, sourceFile: string): Record<string, Record<string, string>> | undefined;
|
|
40
35
|
/**
|
|
@@ -47,8 +42,9 @@ export declare function buildCombinedRouteMapForRouterFile(routerFilePath: strin
|
|
|
47
42
|
};
|
|
48
43
|
/**
|
|
49
44
|
* Walk the full include tree starting from a router file and detect
|
|
50
|
-
* all includes that the static parser cannot resolve
|
|
51
|
-
*
|
|
45
|
+
* all includes that the static parser cannot resolve, plus an explicit
|
|
46
|
+
* createRouter `basename` it cannot read (every pattern would lack the
|
|
47
|
+
* prefix). Returns an array of diagnostics; empty means fully resolvable.
|
|
52
48
|
*/
|
|
53
49
|
export declare function detectUnresolvableIncludes(routerFilePath: string): UnresolvableInclude[];
|
|
54
50
|
/**
|
|
@@ -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:
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The discovery temp RSC server: a minimal Vite server whose module runner
|
|
3
3
|
* imports the user's router files. Shared by dev discovery
|
|
4
|
-
* (discovery/dev-server-phase.ts)
|
|
5
|
-
* (discovery/build-discovery-phase.ts)
|
|
4
|
+
* (discovery/dev-server-phase.ts), build discovery
|
|
5
|
+
* (discovery/build-discovery-phase.ts) and `rango generate --runtime`
|
|
6
|
+
* (build/runtime-discovery.ts).
|
|
6
7
|
*
|
|
7
8
|
* Sits in src/vite/, not src/vite/discovery/: the loader hook URL below
|
|
8
|
-
* resolves against this module's import.meta.url, and the published
|
|
9
|
-
* (dist/vite/index.js)
|
|
9
|
+
* resolves against this module's import.meta.url, and the published bundles
|
|
10
|
+
* (dist/vite/index.js, dist/bin/rango.js) ship the hook at the same relative
|
|
11
|
+
* path.
|
|
10
12
|
*/
|
|
11
13
|
import type { ViteDevServer } from "vite";
|
|
12
14
|
import type { DiscoveryState } from "./discovery/state.js";
|
|
@@ -36,4 +38,6 @@ export declare function createTempRscServer(state: DiscoveryState, options?: {
|
|
|
36
38
|
* shell capture actually runs.
|
|
37
39
|
*/
|
|
38
40
|
realSsrEntry?: boolean;
|
|
41
|
+
/** Caller-specific plugins, appended after the forwarded user plugins. */
|
|
42
|
+
plugins?: import("vite").Plugin[];
|
|
39
43
|
}): Promise<ViteDevServer>;
|
|
@@ -20,6 +20,8 @@ export interface ForwardedRunnerConfig {
|
|
|
20
20
|
/**
|
|
21
21
|
* Extract the data-only config slice to mirror into the discovery temp server.
|
|
22
22
|
* `alias` is included here so callers no longer need to thread it separately.
|
|
23
|
+
* Accepts the unresolved config too: `rango generate --runtime` must not
|
|
24
|
+
* resolve it (that runs rango()'s configResolved, which writes gen files).
|
|
23
25
|
*
|
|
24
26
|
* `tsconfigPaths` is forwarded so Vite 8's native tsconfig `paths` resolution
|
|
25
27
|
* (a top-level `resolve` flag, off by default) reaches the temp server. The
|
|
@@ -34,4 +36,4 @@ export interface ForwardedRunnerConfig {
|
|
|
34
36
|
* replaced the deprecated `esbuild` transform option with `oxc`, so we read and
|
|
35
37
|
* forward `oxc` exclusively — no `esbuild` field is touched.
|
|
36
38
|
*/
|
|
37
|
-
export declare function pickForwardedRunnerConfig(config: ResolvedConfig): ForwardedRunnerConfig;
|
|
39
|
+
export declare function pickForwardedRunnerConfig(config: Pick<ResolvedConfig | UserConfig, "resolve" | "define" | "oxc">): ForwardedRunnerConfig;
|
package/dist/vite/index.js
CHANGED
|
@@ -4841,6 +4841,11 @@ function getAppVersion() {
|
|
|
4841
4841
|
}
|
|
4842
4842
|
|
|
4843
4843
|
// src/router/basename.ts
|
|
4844
|
+
function normalizeBasename(basename2) {
|
|
4845
|
+
if (!basename2) return void 0;
|
|
4846
|
+
const trimmed = basename2.replace(/^\/+|\/+$/g, "");
|
|
4847
|
+
return trimmed ? "/" + trimmed : void 0;
|
|
4848
|
+
}
|
|
4844
4849
|
function prefixBasename(path6, basename2) {
|
|
4845
4850
|
if (!basename2 || !path6.startsWith("/") || path6 === basename2 || path6.startsWith(basename2 + "/") || path6.startsWith(basename2 + "?") || path6.startsWith(basename2 + "#")) {
|
|
4846
4851
|
return path6;
|
|
@@ -5988,7 +5993,7 @@ function serializeSearchSchema(route2, value) {
|
|
|
5988
5993
|
throw projectionError(route2, 'path option "search" must be an object');
|
|
5989
5994
|
}
|
|
5990
5995
|
const entries = [];
|
|
5991
|
-
for (const key of Object.keys(value)
|
|
5996
|
+
for (const key of Object.keys(value)) {
|
|
5992
5997
|
const schemaValue = value[key];
|
|
5993
5998
|
if (typeof schemaValue !== "string" || !SEARCH_SCHEMA_VALUES.has(schemaValue)) {
|
|
5994
5999
|
throw projectionError(
|
|
@@ -7141,7 +7146,7 @@ function resolveVersionsFrom(table, routerId2) {
|
|
|
7141
7146
|
// package.json
|
|
7142
7147
|
var package_default = {
|
|
7143
7148
|
name: "@rangojs/router",
|
|
7144
|
-
version: "0.
|
|
7149
|
+
version: "0.31.0",
|
|
7145
7150
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
7146
7151
|
keywords: [
|
|
7147
7152
|
"react",
|
|
@@ -7334,7 +7339,7 @@ var package_default = {
|
|
|
7334
7339
|
tag: "latest"
|
|
7335
7340
|
},
|
|
7336
7341
|
scripts: {
|
|
7337
|
-
build: "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
7342
|
+
build: "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js && mkdir -p dist/bin/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/bin/plugins/cloudflare-protocol-loader-hook.mjs",
|
|
7338
7343
|
"build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
|
|
7339
7344
|
prepublishOnly: "pnpm build",
|
|
7340
7345
|
typecheck: "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
@@ -8053,20 +8058,39 @@ var SERVER_CSS_MODULE = /^virtual:vite-rsc\/css\?type=rsc&id=([^&]+)/;
|
|
|
8053
8058
|
function isSourceMap(fileName) {
|
|
8054
8059
|
return fileName.endsWith(".map");
|
|
8055
8060
|
}
|
|
8056
|
-
var
|
|
8057
|
-
|
|
8058
|
-
|
|
8061
|
+
var MODULE_MARKER = /^\/\/(#region)? (.+)$/gm;
|
|
8062
|
+
var SERVER_REFERENCE_MAP = new RegExp(
|
|
8063
|
+
`(^//(?:#region)? ${escapeRegExp(`\\0${SERVER_REFERENCES_MODULE_ID.slice(1)}`)}
|
|
8064
|
+
)[\\s\\S]*?(?=^//(?: |#endregion))`,
|
|
8065
|
+
"m"
|
|
8066
|
+
);
|
|
8059
8067
|
function stripServerReferenceMap(source) {
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
const
|
|
8064
|
-
|
|
8065
|
-
|
|
8068
|
+
return source.replace(SERVER_REFERENCE_MAP, "$1");
|
|
8069
|
+
}
|
|
8070
|
+
function printedModuleIds(moduleIds) {
|
|
8071
|
+
const printed = /* @__PURE__ */ new Set();
|
|
8072
|
+
for (const id of moduleIds) {
|
|
8073
|
+
if (!isAbsolutePath(id)) {
|
|
8074
|
+
printed.add(id.replaceAll("\0", "\\0"));
|
|
8075
|
+
continue;
|
|
8076
|
+
}
|
|
8077
|
+
const segments = id.replaceAll("\\", "/").split("/");
|
|
8078
|
+
for (let index = 0; index < segments.length; index++) {
|
|
8079
|
+
printed.add(segments.slice(index).join("/"));
|
|
8080
|
+
}
|
|
8081
|
+
}
|
|
8082
|
+
return printed;
|
|
8066
8083
|
}
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
return source.replace(
|
|
8084
|
+
function stripModulePaths(source, moduleIds) {
|
|
8085
|
+
let printed;
|
|
8086
|
+
return source.replace(
|
|
8087
|
+
MODULE_MARKER,
|
|
8088
|
+
(line, region, name) => {
|
|
8089
|
+
if (region) return "//#region";
|
|
8090
|
+
printed ??= printedModuleIds(moduleIds);
|
|
8091
|
+
return printed.has(name.replace(/^(?:\.\.\/)+/, "")) ? "//" : line;
|
|
8092
|
+
}
|
|
8093
|
+
);
|
|
8070
8094
|
}
|
|
8071
8095
|
var FILE_REFERENCE = "\0";
|
|
8072
8096
|
var FOREIGN_FILE = "~";
|
|
@@ -8118,8 +8142,9 @@ function digestBundle(bundle, projectRoot, read, label) {
|
|
|
8118
8142
|
};
|
|
8119
8143
|
}
|
|
8120
8144
|
const references = [];
|
|
8121
|
-
const text =
|
|
8122
|
-
stripServerReferenceMap(decoder3.decode(bytes))
|
|
8145
|
+
const text = stripModulePaths(
|
|
8146
|
+
stripServerReferenceMap(decoder3.decode(bytes)),
|
|
8147
|
+
chunk.moduleIds
|
|
8123
8148
|
);
|
|
8124
8149
|
const blanked = reference ? text.replace(reference, (name) => {
|
|
8125
8150
|
references.push(fileOfBaseName.get(name));
|
|
@@ -9305,14 +9330,7 @@ function collectCreateRouterCallChain(endpoint) {
|
|
|
9305
9330
|
current = receiver;
|
|
9306
9331
|
}
|
|
9307
9332
|
}
|
|
9308
|
-
function
|
|
9309
|
-
const sourceFile = ts5.createSourceFile(
|
|
9310
|
-
"router.tsx",
|
|
9311
|
-
code,
|
|
9312
|
-
ts5.ScriptTarget.Latest,
|
|
9313
|
-
true,
|
|
9314
|
-
ts5.ScriptKind.TSX
|
|
9315
|
-
);
|
|
9333
|
+
function findRouterUrls(sourceFile) {
|
|
9316
9334
|
let result = null;
|
|
9317
9335
|
function visit(node) {
|
|
9318
9336
|
if (result) return;
|
|
@@ -9340,7 +9358,7 @@ function extractAllUrlsFromRouter(code) {
|
|
|
9340
9358
|
if (urls2) extracted.push(urls2);
|
|
9341
9359
|
}
|
|
9342
9360
|
if (extracted.length > 0) {
|
|
9343
|
-
result = extracted;
|
|
9361
|
+
result = { createRouterCall, extractions: extracted };
|
|
9344
9362
|
return;
|
|
9345
9363
|
}
|
|
9346
9364
|
}
|
|
@@ -9348,13 +9366,9 @@ function extractAllUrlsFromRouter(code) {
|
|
|
9348
9366
|
ts5.forEachChild(node, visit);
|
|
9349
9367
|
}
|
|
9350
9368
|
visit(sourceFile);
|
|
9351
|
-
return result
|
|
9352
|
-
}
|
|
9353
|
-
function extractUrlsFromRouter(code) {
|
|
9354
|
-
const extractions = extractAllUrlsFromRouter(code);
|
|
9355
|
-
return extractions[extractions.length - 1] ?? null;
|
|
9369
|
+
return result;
|
|
9356
9370
|
}
|
|
9357
|
-
function
|
|
9371
|
+
function extractAllUrlsFromRouter(code) {
|
|
9358
9372
|
const sourceFile = ts5.createSourceFile(
|
|
9359
9373
|
"router.tsx",
|
|
9360
9374
|
code,
|
|
@@ -9362,25 +9376,112 @@ function extractBasenameFromRouter(code) {
|
|
|
9362
9376
|
true,
|
|
9363
9377
|
ts5.ScriptKind.TSX
|
|
9364
9378
|
);
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9379
|
+
return findRouterUrls(sourceFile)?.extractions ?? [];
|
|
9380
|
+
}
|
|
9381
|
+
function extractUrlsFromRouter(code) {
|
|
9382
|
+
const extractions = extractAllUrlsFromRouter(code);
|
|
9383
|
+
return extractions[extractions.length - 1] ?? null;
|
|
9384
|
+
}
|
|
9385
|
+
function unwrapTypeOperators(expr) {
|
|
9386
|
+
while (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) {
|
|
9387
|
+
expr = expr.expression;
|
|
9388
|
+
}
|
|
9389
|
+
return expr;
|
|
9390
|
+
}
|
|
9391
|
+
function hasModifier(statement, kind) {
|
|
9392
|
+
return ts5.canHaveModifiers(statement) && (ts5.getModifiers(statement)?.some((m) => m.kind === kind) ?? false);
|
|
9393
|
+
}
|
|
9394
|
+
function findTopLevelConst(sourceFile, name, exported) {
|
|
9395
|
+
for (const statement of sourceFile.statements) {
|
|
9396
|
+
if (ts5.isVariableStatement(statement) && statement.declarationList.flags & ts5.NodeFlags.Const && !hasModifier(statement, ts5.SyntaxKind.DeclareKeyword) && (!exported || hasModifier(statement, ts5.SyntaxKind.ExportKeyword))) {
|
|
9397
|
+
for (const decl of statement.declarationList.declarations) {
|
|
9398
|
+
if (ts5.isIdentifier(decl.name) && decl.name.text === name) {
|
|
9399
|
+
return decl.initializer && unwrapTypeOperators(decl.initializer);
|
|
9377
9400
|
}
|
|
9378
9401
|
}
|
|
9379
9402
|
}
|
|
9380
|
-
ts5.forEachChild(node, visit);
|
|
9381
9403
|
}
|
|
9382
|
-
|
|
9383
|
-
|
|
9404
|
+
return void 0;
|
|
9405
|
+
}
|
|
9406
|
+
function findNamedImport(sourceFile, name) {
|
|
9407
|
+
for (const statement of sourceFile.statements) {
|
|
9408
|
+
if (!ts5.isImportDeclaration(statement) || !ts5.isStringLiteral(statement.moduleSpecifier) || statement.importClause?.isTypeOnly) {
|
|
9409
|
+
continue;
|
|
9410
|
+
}
|
|
9411
|
+
const bindings = statement.importClause?.namedBindings;
|
|
9412
|
+
if (!bindings || !ts5.isNamedImports(bindings)) continue;
|
|
9413
|
+
for (const el of bindings.elements) {
|
|
9414
|
+
if (!el.isTypeOnly && el.name.text === name) {
|
|
9415
|
+
return {
|
|
9416
|
+
specifier: statement.moduleSpecifier.text,
|
|
9417
|
+
exportedName: el.propertyName?.text ?? name
|
|
9418
|
+
};
|
|
9419
|
+
}
|
|
9420
|
+
}
|
|
9421
|
+
}
|
|
9422
|
+
return void 0;
|
|
9423
|
+
}
|
|
9424
|
+
function resolveTopLevelConst(name, sourceFile, routerFilePath) {
|
|
9425
|
+
const local = findTopLevelConst(sourceFile, name, false);
|
|
9426
|
+
if (local) return local;
|
|
9427
|
+
const imported = findNamedImport(sourceFile, name);
|
|
9428
|
+
if (!imported) return void 0;
|
|
9429
|
+
const target = resolveImportPath(imported.specifier, routerFilePath);
|
|
9430
|
+
if (!target) return void 0;
|
|
9431
|
+
const targetSource = readFileSync4(target, "utf-8");
|
|
9432
|
+
return findTopLevelConst(
|
|
9433
|
+
ts5.createSourceFile(target, targetSource, ts5.ScriptTarget.Latest, true),
|
|
9434
|
+
imported.exportedName,
|
|
9435
|
+
true
|
|
9436
|
+
);
|
|
9437
|
+
}
|
|
9438
|
+
function isTopLevelScope(node) {
|
|
9439
|
+
for (let p = node.parent; !ts5.isSourceFile(p); p = p.parent) {
|
|
9440
|
+
if (ts5.isFunctionLike(p) || ts5.isClassLike(p) || ts5.isBlock(p) || ts5.isCaseBlock(p) || ts5.isModuleBlock(p) || ts5.isIterationStatement(p, false)) {
|
|
9441
|
+
return false;
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9444
|
+
return true;
|
|
9445
|
+
}
|
|
9446
|
+
function resolveRouterBasename(routerSource, routerFilePath) {
|
|
9447
|
+
const sourceFile = ts5.createSourceFile(
|
|
9448
|
+
routerFilePath,
|
|
9449
|
+
routerSource,
|
|
9450
|
+
ts5.ScriptTarget.Latest,
|
|
9451
|
+
true,
|
|
9452
|
+
ts5.ScriptKind.TSX
|
|
9453
|
+
);
|
|
9454
|
+
const options = findRouterUrls(sourceFile)?.createRouterCall.arguments[0];
|
|
9455
|
+
const prop = options && ts5.isObjectLiteralExpression(options) ? options.properties.find(
|
|
9456
|
+
(p) => (ts5.isPropertyAssignment(p) || ts5.isShorthandPropertyAssignment(p)) && ts5.isIdentifier(p.name) && p.name.text === "basename"
|
|
9457
|
+
) : void 0;
|
|
9458
|
+
if (!prop) return {};
|
|
9459
|
+
const valueExpr = ts5.isPropertyAssignment(prop) ? prop.initializer : prop.name;
|
|
9460
|
+
const expr = unwrapTypeOperators(valueExpr);
|
|
9461
|
+
let value = getStringValue(expr);
|
|
9462
|
+
if (value === null && isTopLevelScope(prop)) {
|
|
9463
|
+
if (ts5.isIdentifier(expr)) {
|
|
9464
|
+
const init = resolveTopLevelConst(expr.text, sourceFile, routerFilePath);
|
|
9465
|
+
value = init ? getStringValue(init) : null;
|
|
9466
|
+
} else if (ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression)) {
|
|
9467
|
+
const owner = resolveTopLevelConst(
|
|
9468
|
+
expr.expression.text,
|
|
9469
|
+
sourceFile,
|
|
9470
|
+
routerFilePath
|
|
9471
|
+
);
|
|
9472
|
+
value = owner ? resolveConstMember(owner, expr.name.text) : null;
|
|
9473
|
+
}
|
|
9474
|
+
}
|
|
9475
|
+
return value === null ? { unresolved: valueExpr.getText(sourceFile).replace(/\s+/g, " ") } : { value };
|
|
9476
|
+
}
|
|
9477
|
+
function resolveConstMember(owner, name) {
|
|
9478
|
+
if (!ts5.isObjectLiteralExpression(owner) || owner.properties.some(ts5.isSpreadAssignment)) {
|
|
9479
|
+
return null;
|
|
9480
|
+
}
|
|
9481
|
+
const member = owner.properties.find(
|
|
9482
|
+
(p) => ts5.isPropertyAssignment(p) && ts5.isIdentifier(p.name) && p.name.text === name
|
|
9483
|
+
);
|
|
9484
|
+
return member ? getStringValue(unwrapTypeOperators(member.initializer)) : null;
|
|
9384
9485
|
}
|
|
9385
9486
|
function applyBasenameToRoutes(result, basename2) {
|
|
9386
9487
|
const prefixed = {};
|
|
@@ -9478,8 +9579,9 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
|
|
|
9478
9579
|
if (extractions.length === 0) {
|
|
9479
9580
|
return { routes: {}, searchSchemas: {} };
|
|
9480
9581
|
}
|
|
9481
|
-
const
|
|
9482
|
-
|
|
9582
|
+
const basename2 = normalizeBasename(
|
|
9583
|
+
resolveRouterBasename(routerSource, routerFilePath).value
|
|
9584
|
+
);
|
|
9483
9585
|
const result = { routes: {}, searchSchemas: {} };
|
|
9484
9586
|
const memo = createScanMemo();
|
|
9485
9587
|
for (const extraction of extractions) {
|
|
@@ -12673,7 +12775,8 @@ async function createTempRscServer(state, options = {}) {
|
|
|
12673
12775
|
// to resolveId/load (plus transform for import rewriters, see
|
|
12674
12776
|
// IMPORT_REWRITE_PLUGINS) and placed last so framework resolution runs first;
|
|
12675
12777
|
// Vite re-sorts by `enforce`, so `enforce: "pre"` resolvers still lead.
|
|
12676
|
-
...state.userResolvePlugins
|
|
12778
|
+
...state.userResolvePlugins,
|
|
12779
|
+
...options.plugins ?? []
|
|
12677
12780
|
]
|
|
12678
12781
|
});
|
|
12679
12782
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -259,7 +259,7 @@
|
|
|
259
259
|
"node": ">=24.0.0"
|
|
260
260
|
},
|
|
261
261
|
"scripts": {
|
|
262
|
-
"build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
262
|
+
"build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js && mkdir -p dist/bin/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/bin/plugins/cloudflare-protocol-loader-hook.mjs",
|
|
263
263
|
"build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
|
|
264
264
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
265
265
|
"test": "playwright test",
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -445,11 +445,11 @@ npx rango generate src/urls.tsx src/api/
|
|
|
445
445
|
|
|
446
446
|
Three modes:
|
|
447
447
|
|
|
448
|
-
| Flag | Behavior
|
|
449
|
-
| ----------- |
|
|
450
|
-
| (none) | Static parser. Exits with an error, writing nothing, if an `include()` can't be resolved statically (factory or dynamic).
|
|
451
|
-
| `--static` | Static parser, but writes partial output and prints the
|
|
452
|
-
| `--runtime` | Vite-based runtime discovery (full coverage; needs `vite`; `@vitejs/plugin-rsc` comes with the router). `--config <path>` picks the config.
|
|
448
|
+
| Flag | Behavior |
|
|
449
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
450
|
+
| (none) | Static parser. Exits with an error, writing nothing, if an `include()` can't be resolved statically (factory or dynamic), or an explicit `createRouter` `basename` isn't a string literal or top-level const. |
|
|
451
|
+
| `--static` | Static parser, but writes partial output (an unreadable basename leaves patterns unprefixed) and prints the diagnostics as warnings. |
|
|
452
|
+
| `--runtime` | Vite-based runtime discovery (full coverage; needs `vite`; `@vitejs/plugin-rsc` comes with the router). `--config <path>` picks the config. |
|
|
453
453
|
|
|
454
454
|
### Auto-detection
|
|
455
455
|
|
|
@@ -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.
|
|
@@ -111,7 +111,8 @@ imports it directly, so `Rango.Env`, `Rango.Vars`, and optional
|
|
|
111
111
|
and client helpers.
|
|
112
112
|
|
|
113
113
|
CLI flags: the default mode fails (and writes nothing) when an `include()` cannot
|
|
114
|
-
be resolved statically (factory functions, dynamic expressions)
|
|
114
|
+
be resolved statically (factory functions, dynamic expressions) or an explicit
|
|
115
|
+
`createRouter` `basename` is not a string literal or top-level const; `--static`
|
|
115
116
|
accepts partial output with warnings; `--runtime` uses Vite-based discovery for
|
|
116
117
|
full coverage (needs `vite`, optional
|
|
117
118
|
`--config <path>`). Passing a directory scans it for router and `urls()` files.
|
package/src/bin/rango.ts
CHANGED
|
@@ -189,23 +189,23 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
189
189
|
|
|
190
190
|
if (uniqueDiagnostics.length > 0 && mode === "default") {
|
|
191
191
|
// Hard error: no files written
|
|
192
|
-
console.error("\n[rango] Unresolvable
|
|
192
|
+
console.error("\n[rango] Unresolvable routes detected:\n");
|
|
193
193
|
formatDiagnostics(uniqueDiagnostics);
|
|
194
194
|
console.error(
|
|
195
|
-
"\nThe static parser cannot resolve these includes
|
|
196
|
-
"factory functions or dynamic expressions
|
|
195
|
+
"\nThe static parser cannot resolve these includes or basename because " +
|
|
196
|
+
"they use factory functions or dynamic expressions. A basename must be " +
|
|
197
|
+
"a string literal or a top-level const (in the router file or imported " +
|
|
198
|
+
"from a relative module).\n\n" +
|
|
197
199
|
"Options:\n" +
|
|
198
200
|
" rango generate <path> --runtime Use Vite-based discovery (requires vite)\n" +
|
|
199
|
-
" rango generate <path> --static Accept partial output (missing routes above)\n",
|
|
201
|
+
" rango generate <path> --static Accept partial output (missing routes or basename prefix above)\n",
|
|
200
202
|
);
|
|
201
203
|
process.exit(1);
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
if (uniqueDiagnostics.length > 0 && mode === "static") {
|
|
205
207
|
// Warning: partial output accepted
|
|
206
|
-
console.warn(
|
|
207
|
-
"\n[rango] Warning: partial output (unresolvable includes):\n",
|
|
208
|
-
);
|
|
208
|
+
console.warn("\n[rango] Warning: partial output (unresolvable routes):\n");
|
|
209
209
|
formatDiagnostics(uniqueDiagnostics);
|
|
210
210
|
console.warn("");
|
|
211
211
|
}
|
|
@@ -317,7 +317,12 @@ function formatDiagnostics(
|
|
|
317
317
|
diagnostics: Array<UnresolvableInclude & { routerFile: string }>,
|
|
318
318
|
) {
|
|
319
319
|
for (const d of diagnostics) {
|
|
320
|
-
const prefix =
|
|
320
|
+
const prefix =
|
|
321
|
+
d.reason === "unresolvable-basename"
|
|
322
|
+
? "createRouter basename"
|
|
323
|
+
: d.namePrefix
|
|
324
|
+
? `${d.namePrefix}.*`
|
|
325
|
+
: `${d.pathPrefix}*`;
|
|
321
326
|
console.error(` ${prefix}`);
|
|
322
327
|
console.error(` Reason: ${d.reason} -- ${d.detail}`);
|
|
323
328
|
console.error(` Source: ${d.sourceFile}`);
|