@rangojs/router 0.30.1 → 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/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 +119 -36
- package/package.json +2 -2
- package/skills/rango/SKILL.md +5 -5
- 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/vite/temp-rsc-server.ts +9 -4
- package/src/vite/utils/forward-user-plugins.ts +4 -2
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
} from "./include-resolution.js";
|
|
27
27
|
import { findUrlsVariableNames } from "./per-module-writer.js";
|
|
28
28
|
import { isAutoGeneratedRouteName } from "../../route-name.js";
|
|
29
|
+
import { normalizeBasename } from "../../router/basename.js";
|
|
30
|
+
import { getStringValue } from "./ast-helpers.js";
|
|
29
31
|
|
|
30
32
|
function countPublicRouteEntries(source: string): number {
|
|
31
33
|
const matches =
|
|
@@ -232,15 +234,16 @@ function collectCreateRouterCallChain(
|
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
237
|
+
// The first createRouter chain (in source order) that mounts urls, with its
|
|
238
|
+
// extractions in runtime order. Route types and basename both come from it.
|
|
239
|
+
function findRouterUrls(sourceFile: ts.SourceFile): {
|
|
240
|
+
createRouterCall: ts.CallExpression;
|
|
241
|
+
extractions: UrlsExtractionResult[];
|
|
242
|
+
} | null {
|
|
243
|
+
let result: {
|
|
244
|
+
createRouterCall: ts.CallExpression;
|
|
245
|
+
extractions: UrlsExtractionResult[];
|
|
246
|
+
} | null = null;
|
|
244
247
|
|
|
245
248
|
function visit(node: ts.Node) {
|
|
246
249
|
if (result) return;
|
|
@@ -279,7 +282,7 @@ function extractAllUrlsFromRouter(code: string): UrlsExtractionResult[] {
|
|
|
279
282
|
}
|
|
280
283
|
|
|
281
284
|
if (extracted.length > 0) {
|
|
282
|
-
result = extracted;
|
|
285
|
+
result = { createRouterCall, extractions: extracted };
|
|
283
286
|
return;
|
|
284
287
|
}
|
|
285
288
|
}
|
|
@@ -288,7 +291,18 @@ function extractAllUrlsFromRouter(code: string): UrlsExtractionResult[] {
|
|
|
288
291
|
}
|
|
289
292
|
|
|
290
293
|
visit(sourceFile);
|
|
291
|
-
return result
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function extractAllUrlsFromRouter(code: string): UrlsExtractionResult[] {
|
|
298
|
+
const sourceFile = ts.createSourceFile(
|
|
299
|
+
"router.tsx",
|
|
300
|
+
code,
|
|
301
|
+
ts.ScriptTarget.Latest,
|
|
302
|
+
true,
|
|
303
|
+
ts.ScriptKind.TSX,
|
|
304
|
+
);
|
|
305
|
+
return findRouterUrls(sourceFile)?.extractions ?? [];
|
|
292
306
|
}
|
|
293
307
|
|
|
294
308
|
/**
|
|
@@ -307,48 +321,189 @@ export function extractUrlsFromRouter(
|
|
|
307
321
|
return extractions[extractions.length - 1] ?? null;
|
|
308
322
|
}
|
|
309
323
|
|
|
324
|
+
function unwrapTypeOperators(expr: ts.Expression): ts.Expression {
|
|
325
|
+
while (ts.isAsExpression(expr) || ts.isSatisfiesExpression(expr)) {
|
|
326
|
+
expr = expr.expression;
|
|
327
|
+
}
|
|
328
|
+
return expr;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function hasModifier(statement: ts.Statement, kind: ts.SyntaxKind): boolean {
|
|
332
|
+
return (
|
|
333
|
+
ts.canHaveModifiers(statement) &&
|
|
334
|
+
(ts.getModifiers(statement)?.some((m) => m.kind === kind) ?? false)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// `exported` limits the lookup to `export const name = ...`; an export list or
|
|
339
|
+
// re-export is not followed.
|
|
340
|
+
function findTopLevelConst(
|
|
341
|
+
sourceFile: ts.SourceFile,
|
|
342
|
+
name: string,
|
|
343
|
+
exported: boolean,
|
|
344
|
+
): ts.Expression | undefined {
|
|
345
|
+
for (const statement of sourceFile.statements) {
|
|
346
|
+
if (
|
|
347
|
+
ts.isVariableStatement(statement) &&
|
|
348
|
+
statement.declarationList.flags & ts.NodeFlags.Const &&
|
|
349
|
+
!hasModifier(statement, ts.SyntaxKind.DeclareKeyword) &&
|
|
350
|
+
(!exported || hasModifier(statement, ts.SyntaxKind.ExportKeyword))
|
|
351
|
+
) {
|
|
352
|
+
for (const decl of statement.declarationList.declarations) {
|
|
353
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === name) {
|
|
354
|
+
return decl.initializer && unwrapTypeOperators(decl.initializer);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// The value import binding `name` in the router file: a named import
|
|
363
|
+
// (`import { X }` / `import { Y as X }`). Read from the AST, so a commented-out
|
|
364
|
+
// or quoted import never matches. Default and namespace imports are not read.
|
|
365
|
+
function findNamedImport(
|
|
366
|
+
sourceFile: ts.SourceFile,
|
|
367
|
+
name: string,
|
|
368
|
+
): { specifier: string; exportedName: string } | undefined {
|
|
369
|
+
for (const statement of sourceFile.statements) {
|
|
370
|
+
if (
|
|
371
|
+
!ts.isImportDeclaration(statement) ||
|
|
372
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
373
|
+
statement.importClause?.isTypeOnly
|
|
374
|
+
) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const bindings = statement.importClause?.namedBindings;
|
|
378
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
379
|
+
for (const el of bindings.elements) {
|
|
380
|
+
if (!el.isTypeOnly && el.name.text === name) {
|
|
381
|
+
return {
|
|
382
|
+
specifier: statement.moduleSpecifier.text,
|
|
383
|
+
exportedName: el.propertyName?.text ?? name,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// The top-level const `name` refers to: declared in the router file or
|
|
392
|
+
// exported as a const by a relative module it is imported from by name.
|
|
393
|
+
function resolveTopLevelConst(
|
|
394
|
+
name: string,
|
|
395
|
+
sourceFile: ts.SourceFile,
|
|
396
|
+
routerFilePath: string,
|
|
397
|
+
): ts.Expression | undefined {
|
|
398
|
+
const local = findTopLevelConst(sourceFile, name, false);
|
|
399
|
+
if (local) return local;
|
|
400
|
+
const imported = findNamedImport(sourceFile, name);
|
|
401
|
+
if (!imported) return undefined;
|
|
402
|
+
const target = resolveImportPath(imported.specifier, routerFilePath);
|
|
403
|
+
if (!target) return undefined;
|
|
404
|
+
const targetSource = readFileSync(target, "utf-8");
|
|
405
|
+
return findTopLevelConst(
|
|
406
|
+
ts.createSourceFile(target, targetSource, ts.ScriptTarget.Latest, true),
|
|
407
|
+
imported.exportedName,
|
|
408
|
+
true,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Inside any nested scope a local declaration can shadow the top-level
|
|
413
|
+
// binding, and resolving it anyway would emit a wrong prefix.
|
|
414
|
+
function isTopLevelScope(node: ts.Node): boolean {
|
|
415
|
+
for (let p = node.parent; !ts.isSourceFile(p); p = p.parent) {
|
|
416
|
+
if (
|
|
417
|
+
ts.isFunctionLike(p) ||
|
|
418
|
+
ts.isClassLike(p) ||
|
|
419
|
+
ts.isBlock(p) ||
|
|
420
|
+
ts.isCaseBlock(p) ||
|
|
421
|
+
ts.isModuleBlock(p) ||
|
|
422
|
+
ts.isIterationStatement(p, false)
|
|
423
|
+
) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
310
430
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
431
|
+
* Read inline createRouter({ basename: X }) where X is a string literal, a
|
|
432
|
+
* top-level const, or `Const.prop` on a top-level const object literal.
|
|
433
|
+
* `unresolved` is the source of any other X.
|
|
313
434
|
*/
|
|
314
|
-
|
|
435
|
+
function resolveRouterBasename(
|
|
436
|
+
routerSource: string,
|
|
437
|
+
routerFilePath: string,
|
|
438
|
+
): { value?: string; unresolved?: string } {
|
|
315
439
|
const sourceFile = ts.createSourceFile(
|
|
316
|
-
|
|
317
|
-
|
|
440
|
+
routerFilePath,
|
|
441
|
+
routerSource,
|
|
318
442
|
ts.ScriptTarget.Latest,
|
|
319
443
|
true,
|
|
320
444
|
ts.ScriptKind.TSX,
|
|
321
445
|
);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
446
|
+
// Read from the createRouter call whose urls the route types use. Only an
|
|
447
|
+
// explicit `basename` key counts: one inside a spread
|
|
448
|
+
// (`createRouter({ ...shared })`) or a non-literal options argument stays
|
|
449
|
+
// unread and unreported.
|
|
450
|
+
const options = findRouterUrls(sourceFile)?.createRouterCall.arguments[0];
|
|
451
|
+
const prop =
|
|
452
|
+
options && ts.isObjectLiteralExpression(options)
|
|
453
|
+
? options.properties.find(
|
|
454
|
+
(p): p is ts.PropertyAssignment | ts.ShorthandPropertyAssignment =>
|
|
455
|
+
(ts.isPropertyAssignment(p) ||
|
|
456
|
+
ts.isShorthandPropertyAssignment(p)) &&
|
|
457
|
+
ts.isIdentifier(p.name) &&
|
|
458
|
+
p.name.text === "basename",
|
|
459
|
+
)
|
|
460
|
+
: undefined;
|
|
461
|
+
if (!prop) return {};
|
|
462
|
+
|
|
463
|
+
const valueExpr = ts.isPropertyAssignment(prop)
|
|
464
|
+
? prop.initializer
|
|
465
|
+
: prop.name;
|
|
466
|
+
const expr = unwrapTypeOperators(valueExpr);
|
|
467
|
+
let value = getStringValue(expr);
|
|
468
|
+
if (value === null && isTopLevelScope(prop)) {
|
|
469
|
+
if (ts.isIdentifier(expr)) {
|
|
470
|
+
const init = resolveTopLevelConst(expr.text, sourceFile, routerFilePath);
|
|
471
|
+
value = init ? getStringValue(init) : null;
|
|
472
|
+
} else if (
|
|
473
|
+
ts.isPropertyAccessExpression(expr) &&
|
|
474
|
+
ts.isIdentifier(expr.expression)
|
|
330
475
|
) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
prop.name.text === "basename" &&
|
|
338
|
-
ts.isStringLiteral(prop.initializer)
|
|
339
|
-
) {
|
|
340
|
-
result = prop.initializer.text;
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
476
|
+
const owner = resolveTopLevelConst(
|
|
477
|
+
expr.expression.text,
|
|
478
|
+
sourceFile,
|
|
479
|
+
routerFilePath,
|
|
480
|
+
);
|
|
481
|
+
value = owner ? resolveConstMember(owner, expr.name.text) : null;
|
|
346
482
|
}
|
|
347
|
-
ts.forEachChild(node, visit);
|
|
348
483
|
}
|
|
484
|
+
return value === null
|
|
485
|
+
? { unresolved: valueExpr.getText(sourceFile).replace(/\s+/g, " ") }
|
|
486
|
+
: { value };
|
|
487
|
+
}
|
|
349
488
|
|
|
350
|
-
|
|
351
|
-
|
|
489
|
+
// String value of `owner.name` when owner is an object literal. A spread may
|
|
490
|
+
// override the property, so an owner containing one is not resolved.
|
|
491
|
+
function resolveConstMember(owner: ts.Expression, name: string): string | null {
|
|
492
|
+
if (
|
|
493
|
+
!ts.isObjectLiteralExpression(owner) ||
|
|
494
|
+
owner.properties.some(ts.isSpreadAssignment)
|
|
495
|
+
) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
const member = owner.properties.find(
|
|
499
|
+
(p): p is ts.PropertyAssignment =>
|
|
500
|
+
ts.isPropertyAssignment(p) &&
|
|
501
|
+
ts.isIdentifier(p.name) &&
|
|
502
|
+
p.name.text === name,
|
|
503
|
+
);
|
|
504
|
+
return member
|
|
505
|
+
? getStringValue(unwrapTypeOperators(member.initializer))
|
|
506
|
+
: null;
|
|
352
507
|
}
|
|
353
508
|
|
|
354
509
|
/** Apply a basename prefix to all route patterns in a result set. */
|
|
@@ -496,11 +651,9 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
|
496
651
|
return { routes: {}, searchSchemas: {} };
|
|
497
652
|
}
|
|
498
653
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
|
|
503
|
-
: undefined;
|
|
654
|
+
const basename = normalizeBasename(
|
|
655
|
+
resolveRouterBasename(routerSource, routerFilePath).value,
|
|
656
|
+
);
|
|
504
657
|
|
|
505
658
|
const result: RouteMapWithSearch = { routes: {}, searchSchemas: {} };
|
|
506
659
|
const memo = createScanMemo();
|
|
@@ -531,8 +684,9 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
|
531
684
|
|
|
532
685
|
/**
|
|
533
686
|
* Walk the full include tree starting from a router file and detect
|
|
534
|
-
* all includes that the static parser cannot resolve
|
|
535
|
-
*
|
|
687
|
+
* all includes that the static parser cannot resolve, plus an explicit
|
|
688
|
+
* createRouter `basename` it cannot read (every pattern would lack the
|
|
689
|
+
* prefix). Returns an array of diagnostics; empty means fully resolvable.
|
|
536
690
|
*/
|
|
537
691
|
export function detectUnresolvableIncludes(
|
|
538
692
|
routerFilePath: string,
|
|
@@ -559,6 +713,16 @@ export function detectUnresolvableIncludes(
|
|
|
559
713
|
memo,
|
|
560
714
|
);
|
|
561
715
|
}
|
|
716
|
+
const { unresolved } = resolveRouterBasename(source, realPath);
|
|
717
|
+
if (unresolved !== undefined) {
|
|
718
|
+
diagnostics.push({
|
|
719
|
+
pathPrefix: "/",
|
|
720
|
+
namePrefix: null,
|
|
721
|
+
reason: "unresolvable-basename",
|
|
722
|
+
sourceFile: realPath,
|
|
723
|
+
detail: `basename \`${unresolved}\` is not a string literal or top-level const; every pattern would lack the prefix`,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
562
726
|
return diagnostics;
|
|
563
727
|
}
|
|
564
728
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import type { Plugin, PluginOption, ViteDevServer } from "vite";
|
|
3
4
|
import {
|
|
4
5
|
generateRouteTypesSource,
|
|
5
6
|
genFileTsPath,
|
|
@@ -35,13 +36,10 @@ export async function discoverAndWriteRouteTypes(
|
|
|
35
36
|
routeCount: number;
|
|
36
37
|
outputFiles: string[];
|
|
37
38
|
}> {
|
|
38
|
-
let createViteServer: typeof import("vite").createServer;
|
|
39
39
|
let loadConfigFromFile: typeof import("vite").loadConfigFromFile;
|
|
40
|
-
let rsc: any;
|
|
41
40
|
|
|
42
41
|
try {
|
|
43
42
|
const vite = await import("vite");
|
|
44
|
-
createViteServer = vite.createServer;
|
|
45
43
|
loadConfigFromFile = vite.loadConfigFromFile;
|
|
46
44
|
} catch {
|
|
47
45
|
throw new Error(
|
|
@@ -50,61 +48,57 @@ export async function discoverAndWriteRouteTypes(
|
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
try {
|
|
53
|
-
|
|
54
|
-
rsc = rscMod.default;
|
|
51
|
+
await import("@vitejs/plugin-rsc");
|
|
55
52
|
} catch {
|
|
56
53
|
throw new Error(
|
|
57
54
|
"Runtime discovery requires '@vitejs/plugin-rsc'. Install it with: pnpm add -D @vitejs/plugin-rsc",
|
|
58
55
|
);
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
const {
|
|
64
|
-
await import("../vite/
|
|
58
|
+
const { createTempRscServer } = await import("../vite/temp-rsc-server.js");
|
|
59
|
+
const { createDiscoveryState } = await import("../vite/discovery/state.js");
|
|
60
|
+
const { discoverClientUrlProjections } =
|
|
61
|
+
await import("../vite/discovery/client-urls-projection.js");
|
|
62
|
+
const { pickForwardedRunnerConfig, selectForwardableResolvePlugins } =
|
|
63
|
+
await import("../vite/utils/forward-user-plugins.js");
|
|
65
64
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
// The discovery temp server of `vite dev/build` (expose-internal-ids, the
|
|
66
|
+
// cloudflare:* stubs, the user's resolver plugins and resolve/define/oxc).
|
|
67
|
+
// The config stays unresolved: resolving it would run rango()'s
|
|
68
|
+
// configResolved, which writes the static gen files. Forwarded resolvers
|
|
69
|
+
// therefore run without their own config/configResolved hooks.
|
|
70
|
+
const entryPath = resolve(opts.entry);
|
|
71
|
+
const state = createDiscoveryState(entryPath, undefined);
|
|
72
|
+
state.projectRoot = opts.root;
|
|
73
|
+
state.userResolveAlias = opts.resolveAlias;
|
|
74
|
+
if (!opts.resolveAlias) {
|
|
70
75
|
try {
|
|
71
76
|
const loaded = await loadConfigFromFile(
|
|
72
77
|
{ command: "serve", mode: "development" },
|
|
73
|
-
|
|
78
|
+
opts.configFile,
|
|
74
79
|
opts.root,
|
|
75
80
|
);
|
|
76
|
-
if (loaded
|
|
77
|
-
|
|
81
|
+
if (loaded) {
|
|
82
|
+
state.userRunnerConfig = pickForwardedRunnerConfig(loaded.config);
|
|
83
|
+
state.userResolvePlugins = selectForwardableResolvePlugins(
|
|
84
|
+
await flattenPluginOptions(loaded.config.plugins),
|
|
85
|
+
);
|
|
78
86
|
}
|
|
79
|
-
} catch {
|
|
80
|
-
//
|
|
87
|
+
} catch (err: any) {
|
|
88
|
+
// Discovery without the app's resolvers fails later on an unrelated
|
|
89
|
+
// import; name the cause here.
|
|
90
|
+
console.warn(
|
|
91
|
+
`[rango] Could not load the Vite config (${err?.message ?? err}); discovering without its resolve config and plugins`,
|
|
92
|
+
);
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
let tempServer: any = null;
|
|
96
|
+
let tempServer: ViteDevServer | null = null;
|
|
86
97
|
|
|
87
98
|
try {
|
|
88
|
-
tempServer = await
|
|
89
|
-
root: opts.root,
|
|
90
|
-
configFile: false,
|
|
91
|
-
server: { middlewareMode: true },
|
|
92
|
-
appType: "custom",
|
|
93
|
-
logLevel: "silent",
|
|
99
|
+
tempServer = await createTempRscServer(state, {
|
|
94
100
|
cacheDir: "node_modules/.vite_rango_generate",
|
|
95
|
-
|
|
96
|
-
esbuild: { jsx: "automatic", jsxImportSource: "react" },
|
|
97
|
-
plugins: [
|
|
98
|
-
rsc({
|
|
99
|
-
entries: {
|
|
100
|
-
client: "virtual:entry-client",
|
|
101
|
-
ssr: "virtual:entry-ssr",
|
|
102
|
-
rsc: entryPath,
|
|
103
|
-
},
|
|
104
|
-
}),
|
|
105
|
-
createVersionPlugin(),
|
|
106
|
-
createVirtualStubPlugin(),
|
|
107
|
-
],
|
|
101
|
+
plugins: [missingGenFileStubPlugin()],
|
|
108
102
|
});
|
|
109
103
|
|
|
110
104
|
const rscEnv = (tempServer.environments as any)?.rsc;
|
|
@@ -125,6 +119,14 @@ export async function discoverAndWriteRouteTypes(
|
|
|
125
119
|
);
|
|
126
120
|
}
|
|
127
121
|
|
|
122
|
+
// include() of a clientUrls() module resolves from these projections.
|
|
123
|
+
await discoverClientUrlProjections(
|
|
124
|
+
state,
|
|
125
|
+
(tempServer.environments as any)?.ssr,
|
|
126
|
+
serverMod,
|
|
127
|
+
rscEnv,
|
|
128
|
+
);
|
|
129
|
+
|
|
128
130
|
// Import build utilities for manifest generation
|
|
129
131
|
const buildMod = await rscEnv.runner.import("@rangojs/router/build");
|
|
130
132
|
const generateManifestFull = buildMod.generateManifestFull;
|
|
@@ -149,7 +151,11 @@ export async function discoverAndWriteRouteTypes(
|
|
|
149
151
|
const mountManifests = [];
|
|
150
152
|
for (const mount of mounts) {
|
|
151
153
|
mountManifests.push(
|
|
152
|
-
await generateManifestFull(
|
|
154
|
+
await generateManifestFull(
|
|
155
|
+
mount.patterns,
|
|
156
|
+
globalMountIndex,
|
|
157
|
+
router.__basename ? { urlPrefix: router.__basename } : undefined,
|
|
158
|
+
),
|
|
153
159
|
);
|
|
154
160
|
globalMountIndex++;
|
|
155
161
|
}
|
|
@@ -232,3 +238,46 @@ export async function discoverAndWriteRouteTypes(
|
|
|
232
238
|
}
|
|
233
239
|
}
|
|
234
240
|
}
|
|
241
|
+
|
|
242
|
+
const MISSING_GEN_PREFIX = "\0rango-missing-named-routes-gen:";
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* expose-router-id makes every createRouter() file import its sibling
|
|
246
|
+
* `./<name>.named-routes.gen.js`. `vite dev/build` write those from static
|
|
247
|
+
* parsing before discovery; the CLI does not, so a missing one resolves to an
|
|
248
|
+
* empty NamedRoutes here and discovery writes the real file. An existing gen
|
|
249
|
+
* file loads from disk as usual.
|
|
250
|
+
*/
|
|
251
|
+
function missingGenFileStubPlugin(): Plugin {
|
|
252
|
+
return {
|
|
253
|
+
name: "@rangojs/router:missing-gen-file-stub",
|
|
254
|
+
enforce: "pre",
|
|
255
|
+
resolveId(id, importer) {
|
|
256
|
+
if (!importer || !id.endsWith(".named-routes.gen.js")) return null;
|
|
257
|
+
const genPath = genFileTsPath(importer.split("?", 1)[0]);
|
|
258
|
+
if (id !== `./${basename(genPath, ".ts")}.js` || existsSync(genPath)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return MISSING_GEN_PREFIX + genPath;
|
|
262
|
+
},
|
|
263
|
+
load(id) {
|
|
264
|
+
return id.startsWith(MISSING_GEN_PREFIX)
|
|
265
|
+
? "export const NamedRoutes = {};\n"
|
|
266
|
+
: null;
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Flatten an unresolved `plugins` option (nested arrays, promises, falsy
|
|
273
|
+
* entries) the way Vite's config resolution does, without running any hook.
|
|
274
|
+
*/
|
|
275
|
+
async function flattenPluginOptions(
|
|
276
|
+
option: PluginOption | undefined,
|
|
277
|
+
): Promise<Plugin[]> {
|
|
278
|
+
const value = await option;
|
|
279
|
+
if (Array.isArray(value)) {
|
|
280
|
+
return (await Promise.all(value.map(flattenPluginOptions))).flat();
|
|
281
|
+
}
|
|
282
|
+
return value ? [value as Plugin] : [];
|
|
283
|
+
}
|
|
@@ -170,8 +170,10 @@ function serializeSearchSchema(
|
|
|
170
170
|
throw projectionError(route, 'path option "search" must be an object');
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
// Source order, not sorted: the build writes the gen file's search keys from
|
|
174
|
+
// this projection and must match `rango generate`'s static parse (#1322).
|
|
173
175
|
const entries: Array<[string, SearchSchemaValue]> = [];
|
|
174
|
-
for (const key of Object.keys(value)
|
|
176
|
+
for (const key of Object.keys(value)) {
|
|
175
177
|
const schemaValue = (value as Record<string, unknown>)[key];
|
|
176
178
|
if (
|
|
177
179
|
typeof schemaValue !== "string" ||
|
|
@@ -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
|
|
|
12
14
|
import type { ViteDevServer } from "vite";
|
|
@@ -103,6 +105,8 @@ export async function createTempRscServer(
|
|
|
103
105
|
* shell capture actually runs.
|
|
104
106
|
*/
|
|
105
107
|
realSsrEntry?: boolean;
|
|
108
|
+
/** Caller-specific plugins, appended after the forwarded user plugins. */
|
|
109
|
+
plugins?: import("vite").Plugin[];
|
|
106
110
|
} = {},
|
|
107
111
|
): Promise<ViteDevServer> {
|
|
108
112
|
// Install the Node ESM loader hook before any module evaluation so
|
|
@@ -218,6 +222,7 @@ export async function createTempRscServer(
|
|
|
218
222
|
// IMPORT_REWRITE_PLUGINS) and placed last so framework resolution runs first;
|
|
219
223
|
// Vite re-sorts by `enforce`, so `enforce: "pre"` resolvers still lead.
|
|
220
224
|
...state.userResolvePlugins,
|
|
225
|
+
...(options.plugins ?? []),
|
|
221
226
|
],
|
|
222
227
|
});
|
|
223
228
|
}
|
|
@@ -139,6 +139,8 @@ export interface ForwardedRunnerConfig {
|
|
|
139
139
|
/**
|
|
140
140
|
* Extract the data-only config slice to mirror into the discovery temp server.
|
|
141
141
|
* `alias` is included here so callers no longer need to thread it separately.
|
|
142
|
+
* Accepts the unresolved config too: `rango generate --runtime` must not
|
|
143
|
+
* resolve it (that runs rango()'s configResolved, which writes gen files).
|
|
142
144
|
*
|
|
143
145
|
* `tsconfigPaths` is forwarded so Vite 8's native tsconfig `paths` resolution
|
|
144
146
|
* (a top-level `resolve` flag, off by default) reaches the temp server. The
|
|
@@ -154,9 +156,9 @@ export interface ForwardedRunnerConfig {
|
|
|
154
156
|
* forward `oxc` exclusively — no `esbuild` field is touched.
|
|
155
157
|
*/
|
|
156
158
|
export function pickForwardedRunnerConfig(
|
|
157
|
-
config: ResolvedConfig,
|
|
159
|
+
config: Pick<ResolvedConfig | UserConfig, "resolve" | "define" | "oxc">,
|
|
158
160
|
): ForwardedRunnerConfig {
|
|
159
|
-
const r = config.resolve ??
|
|
161
|
+
const r = config.resolve ?? {};
|
|
160
162
|
const resolve: NonNullable<UserConfig["resolve"]> = {};
|
|
161
163
|
if (r.alias !== undefined) resolve.alias = r.alias as any;
|
|
162
164
|
if (r.dedupe !== undefined) resolve.dedupe = r.dedupe;
|