@rangojs/router 0.0.0-experimental.130 → 0.0.0-experimental.132

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 (54) hide show
  1. package/dist/bin/rango.js +69 -24
  2. package/dist/vite/index.js +182 -41
  3. package/package.json +6 -3
  4. package/src/browser/connection-warmup.ts +134 -0
  5. package/src/browser/event-controller.ts +5 -4
  6. package/src/browser/partial-update.ts +32 -16
  7. package/src/browser/react/NavigationProvider.tsx +6 -83
  8. package/src/browser/react/filter-segment-order.ts +17 -0
  9. package/src/browser/react/use-link-status.ts +10 -2
  10. package/src/browser/react/use-navigation.ts +10 -2
  11. package/src/build/route-types/ast-route-extraction.ts +15 -8
  12. package/src/build/route-types/include-resolution.ts +109 -21
  13. package/src/build/route-types/per-module-writer.ts +15 -2
  14. package/src/cache/cache-key-utils.ts +29 -13
  15. package/src/cache/cf/cf-cache-store.ts +129 -5
  16. package/src/decode-loader-results.ts +11 -1
  17. package/src/encode-kv.ts +49 -0
  18. package/src/handles/meta.ts +5 -1
  19. package/src/host/cookie-handler.ts +2 -21
  20. package/src/prerender/param-hash.ts +6 -5
  21. package/src/regex-escape.ts +8 -0
  22. package/src/route-definition/dsl-helpers.ts +6 -2
  23. package/src/router/error-handling.ts +32 -1
  24. package/src/router/handler-context.ts +6 -1
  25. package/src/router/instrument.ts +56 -14
  26. package/src/router/intercept-resolution.ts +16 -1
  27. package/src/router/loader-resolution.ts +49 -19
  28. package/src/router/match-middleware/background-revalidation.ts +6 -0
  29. package/src/router/match-middleware/cache-store.ts +6 -0
  30. package/src/router/middleware.ts +67 -27
  31. package/src/router/pattern-matching.ts +3 -9
  32. package/src/router/revalidation.ts +65 -23
  33. package/src/router/router-context.ts +1 -0
  34. package/src/router/router-options.ts +3 -3
  35. package/src/router/segment-resolution/fresh.ts +8 -9
  36. package/src/router/segment-resolution/helpers.ts +11 -10
  37. package/src/router/segment-resolution/loader-cache.ts +13 -0
  38. package/src/router/segment-resolution/revalidation.ts +4 -4
  39. package/src/router/segment-wrappers.ts +3 -0
  40. package/src/router/trie-matching.ts +74 -20
  41. package/src/router.ts +2 -2
  42. package/src/rsc/progressive-enhancement.ts +20 -0
  43. package/src/rsc/server-action.ts +124 -47
  44. package/src/search-params.ts +8 -6
  45. package/src/segment-system.tsx +7 -1
  46. package/src/server/cookie-parse.ts +32 -0
  47. package/src/server/handle-store.ts +14 -14
  48. package/src/server/request-context.ts +5 -26
  49. package/src/ssr/index.tsx +5 -4
  50. package/src/testing/render-handler.ts +11 -0
  51. package/src/vite/plugins/expose-id-utils.ts +77 -2
  52. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -5
  53. package/src/vite/plugins/expose-ids/router-transform.ts +82 -12
  54. package/src/vite/utils/prerender-utils.ts +1 -3
package/dist/bin/rango.js CHANGED
@@ -1,13 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res, err) => function __init() {
5
- if (err) throw err[0];
6
- try {
7
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
- } catch (e) {
9
- throw err = [e], e;
10
- }
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
6
  };
12
7
  var __export = (target, all) => {
13
8
  for (var name in all)
@@ -64,8 +59,8 @@ var init_ast_helpers = __esm({
64
59
 
65
60
  // src/build/route-types/ast-route-extraction.ts
66
61
  import ts2 from "typescript";
67
- function extractRoutesFromSource(code) {
68
- const sourceFile = ts2.createSourceFile(
62
+ function extractRoutesFromSource(code, sourceFileArg) {
63
+ const sourceFile = sourceFileArg ?? ts2.createSourceFile(
69
64
  "input.tsx",
70
65
  code,
71
66
  ts2.ScriptTarget.Latest,
@@ -247,6 +242,33 @@ var init_scan_filter = __esm({
247
242
  import { readFileSync, existsSync } from "node:fs";
248
243
  import { dirname, resolve } from "node:path";
249
244
  import ts3 from "typescript";
245
+ function createScanMemo() {
246
+ return { files: /* @__PURE__ */ new Map(), blockSourceFiles: /* @__PURE__ */ new Map() };
247
+ }
248
+ function parseBlock(memo, block) {
249
+ if (memo) {
250
+ const cached = memo.blockSourceFiles.get(block);
251
+ if (cached) return cached;
252
+ }
253
+ const sf = ts3.createSourceFile(
254
+ "input.tsx",
255
+ block,
256
+ ts3.ScriptTarget.Latest,
257
+ true,
258
+ ts3.ScriptKind.TSX
259
+ );
260
+ if (memo) memo.blockSourceFiles.set(block, sf);
261
+ return sf;
262
+ }
263
+ function readSourceMemoized(memo, realPath) {
264
+ if (memo) {
265
+ const cached = memo.files.get(realPath);
266
+ if (cached !== void 0) return cached;
267
+ }
268
+ const source = readFileSync(realPath, "utf-8");
269
+ if (memo) memo.files.set(realPath, source);
270
+ return source;
271
+ }
250
272
  function extractNamePrefixFromInclude(node) {
251
273
  if (node.arguments.length >= 3) {
252
274
  const thirdArg = node.arguments[2];
@@ -262,8 +284,8 @@ function extractNamePrefixFromInclude(node) {
262
284
  }
263
285
  return null;
264
286
  }
265
- function extractIncludesWithDiagnostics(code) {
266
- const sourceFile = ts3.createSourceFile(
287
+ function extractIncludesWithDiagnostics(code, sourceFileArg) {
288
+ const sourceFile = sourceFileArg ?? ts3.createSourceFile(
267
289
  "input.tsx",
268
290
  code,
269
291
  ts3.ScriptTarget.Latest,
@@ -354,8 +376,8 @@ function resolveImportPath(importSpec, fromFile) {
354
376
  }
355
377
  return null;
356
378
  }
357
- function extractUrlsBlockForVariable(code, varName) {
358
- const sourceFile = ts3.createSourceFile(
379
+ function extractUrlsBlockForVariable(code, varName, sourceFileArg) {
380
+ const sourceFile = sourceFileArg ?? ts3.createSourceFile(
359
381
  "input.tsx",
360
382
  code,
361
383
  ts3.ScriptTarget.Latest,
@@ -377,16 +399,20 @@ function extractUrlsBlockForVariable(code, varName) {
377
399
  visit(sourceFile);
378
400
  return result;
379
401
  }
380
- function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSchemasOut, diagnosticsOut) {
402
+ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSchemasOut, diagnosticsOut, memo) {
381
403
  const routeMap = {};
382
- const localRoutes = extractRoutesFromSource(block);
404
+ const blockSourceFile = parseBlock(memo, block);
405
+ const localRoutes = extractRoutesFromSource(block, blockSourceFile);
383
406
  for (const { name, pattern, search } of localRoutes) {
384
407
  routeMap[name] = pattern;
385
408
  if (search && searchSchemasOut) {
386
409
  searchSchemasOut[name] = search;
387
410
  }
388
411
  }
389
- const { resolved: includes, unresolvable } = extractIncludesWithDiagnostics(block);
412
+ const { resolved: includes, unresolvable } = extractIncludesWithDiagnostics(
413
+ block,
414
+ blockSourceFile
415
+ );
390
416
  if (diagnosticsOut) {
391
417
  for (const entry of unresolvable) {
392
418
  diagnosticsOut.push({ ...entry, sourceFile: filePath });
@@ -413,12 +439,15 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
413
439
  targetFile,
414
440
  imported.exportedName,
415
441
  visited,
416
- diagnosticsOut
442
+ diagnosticsOut,
443
+ void 0,
444
+ memo
417
445
  );
418
446
  } else {
419
447
  const sameFileBlock = extractUrlsBlockForVariable(
420
448
  fullSource,
421
- variableName
449
+ variableName,
450
+ parseBlock(memo, fullSource)
422
451
  );
423
452
  if (!sameFileBlock) {
424
453
  if (diagnosticsOut) {
@@ -436,7 +465,9 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
436
465
  filePath,
437
466
  variableName,
438
467
  visited,
439
- diagnosticsOut
468
+ diagnosticsOut,
469
+ void 0,
470
+ memo
440
471
  );
441
472
  }
442
473
  if (namePrefix === null) {
@@ -460,8 +491,9 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
460
491
  }
461
492
  return routeMap;
462
493
  }
463
- function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock) {
494
+ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock, memo) {
464
495
  visited = visited ?? /* @__PURE__ */ new Set();
496
+ memo = memo ?? createScanMemo();
465
497
  const realPath = resolve(filePath);
466
498
  const key = variableName ? `${realPath}:${variableName}` : realPath;
467
499
  if (visited.has(key)) {
@@ -471,7 +503,7 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
471
503
  visited.add(key);
472
504
  let source;
473
505
  try {
474
- source = readFileSync(realPath, "utf-8");
506
+ source = readSourceMemoized(memo, realPath);
475
507
  } catch {
476
508
  return { routes: {}, searchSchemas: {} };
477
509
  }
@@ -479,7 +511,11 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
479
511
  if (inlineBlock) {
480
512
  block = inlineBlock;
481
513
  } else if (variableName) {
482
- const extracted = extractUrlsBlockForVariable(source, variableName);
514
+ const extracted = extractUrlsBlockForVariable(
515
+ source,
516
+ variableName,
517
+ parseBlock(memo, source)
518
+ );
483
519
  if (!extracted) return { routes: {}, searchSchemas: {} };
484
520
  block = extracted;
485
521
  } else {
@@ -492,7 +528,8 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
492
528
  realPath,
493
529
  visited,
494
530
  searchSchemas,
495
- diagnosticsOut
531
+ diagnosticsOut,
532
+ memo
496
533
  );
497
534
  visited.delete(key);
498
535
  return { routes, searchSchemas };
@@ -537,8 +574,16 @@ function writePerModuleRouteTypesForFile(filePath) {
537
574
  let routes;
538
575
  if (varNames.length > 0) {
539
576
  routes = [];
577
+ const memo = createScanMemo();
540
578
  for (const varName of varNames) {
541
- const { routes: routeMap, searchSchemas } = buildCombinedRouteMapWithSearch(filePath, varName);
579
+ const { routes: routeMap, searchSchemas } = buildCombinedRouteMapWithSearch(
580
+ filePath,
581
+ varName,
582
+ void 0,
583
+ void 0,
584
+ void 0,
585
+ memo
586
+ );
542
587
  for (const [name, pattern] of Object.entries(routeMap)) {
543
588
  const params = extractParamsFromPattern(pattern);
544
589
  routes.push({
@@ -10,6 +10,13 @@ import fs from "node:fs";
10
10
  // src/vite/plugins/expose-id-utils.ts
11
11
  import path from "node:path";
12
12
  import crypto from "node:crypto";
13
+
14
+ // src/regex-escape.ts
15
+ function escapeRegExp(input) {
16
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
+ }
18
+
19
+ // src/vite/plugins/expose-id-utils.ts
13
20
  function normalizePath(p) {
14
21
  return p.split(path.sep).join("/");
15
22
  }
@@ -187,8 +194,59 @@ function findStatementEnd(code, pos) {
187
194
  }
188
195
  return i;
189
196
  }
190
- function escapeRegExp(input) {
191
- return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
197
+ function findCallParenAfterGenerics(code, afterCalleeIndex) {
198
+ let i = afterCalleeIndex;
199
+ while (i < code.length) {
200
+ const skipped = skipStringOrComment(code, i);
201
+ if (skipped > i) {
202
+ i = skipped;
203
+ continue;
204
+ }
205
+ if (/\s/.test(code[i])) {
206
+ i++;
207
+ continue;
208
+ }
209
+ break;
210
+ }
211
+ if (i >= code.length) return -1;
212
+ if (code[i] === "(") return i;
213
+ if (code[i] === "<") {
214
+ let depth = 0;
215
+ while (i < code.length) {
216
+ const skipped = skipStringOrComment(code, i);
217
+ if (skipped > i) {
218
+ i = skipped;
219
+ continue;
220
+ }
221
+ const ch = code[i];
222
+ if (ch === "<") {
223
+ depth++;
224
+ } else if (ch === ">") {
225
+ depth--;
226
+ if (depth === 0) {
227
+ i++;
228
+ break;
229
+ }
230
+ }
231
+ i++;
232
+ }
233
+ if (depth !== 0) return -1;
234
+ while (i < code.length) {
235
+ const skipped = skipStringOrComment(code, i);
236
+ if (skipped > i) {
237
+ i = skipped;
238
+ continue;
239
+ }
240
+ if (/\s/.test(code[i])) {
241
+ i++;
242
+ continue;
243
+ }
244
+ break;
245
+ }
246
+ if (i < code.length && code[i] === "(") return i;
247
+ return -1;
248
+ }
249
+ return -1;
192
250
  }
193
251
 
194
252
  // src/vite/debug.ts
@@ -850,13 +908,23 @@ function isExportOnlyFile(code, bindings) {
850
908
  return true;
851
909
  }
852
910
  function createCallPattern(fnNames) {
853
- return new RegExp(
854
- `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
855
- "g"
911
+ return new RegExp(`\\b(?:${fnNames.map(escapeRegExp).join("|")})\\b`, "g");
912
+ }
913
+ function createCallStartIndices(code, fnNames) {
914
+ return codeMatchIndices(code, createCallPattern(fnNames)).filter(
915
+ (index) => findCallParenAfterGenerics(
916
+ code,
917
+ index + matchedNameLength(code, index)
918
+ ) !== -1
856
919
  );
857
920
  }
921
+ function matchedNameLength(code, index) {
922
+ let i = index;
923
+ while (i < code.length && /[A-Za-z0-9_$]/.test(code[i])) i++;
924
+ return i - index;
925
+ }
858
926
  function countCreateCallsForNames(code, fnNames) {
859
- return codeMatchIndices(code, createCallPattern(fnNames)).length;
927
+ return createCallStartIndices(code, fnNames).length;
860
928
  }
861
929
  function offsetToLineColumn(code, index) {
862
930
  let line = 1;
@@ -872,7 +940,7 @@ function offsetToLineColumn(code, index) {
872
940
  }
873
941
  function findUnsupportedCreateCallSites(code, fnNames, supportedBindings) {
874
942
  const supported = new Set(supportedBindings.map((b) => b.callExprStart));
875
- return codeMatchIndices(code, createCallPattern(fnNames)).filter((index) => !supported.has(index)).map((index) => offsetToLineColumn(code, index));
943
+ return createCallStartIndices(code, fnNames).filter((index) => !supported.has(index)).map((index) => offsetToLineColumn(code, index));
876
944
  }
877
945
  function getImportedFnNames(code, importedName) {
878
946
  const importPattern = /import\s*\{([^}]*)\}\s*from\s*["']@rangojs\/router(?:\/[^"']*)?["']/g;
@@ -1181,14 +1249,31 @@ import MagicString2 from "magic-string";
1181
1249
  import path3 from "node:path";
1182
1250
  import { createHash } from "node:crypto";
1183
1251
  var debug2 = createRangoDebugger(NS.transform);
1184
- function transformRouter(code, filePath, routerFnNames, absolutePath) {
1252
+ function skipLeadingTrivia(code, start, end) {
1253
+ let i = start;
1254
+ while (i < end) {
1255
+ const skipped = skipStringOrComment(code, i);
1256
+ if (skipped > i) {
1257
+ i = skipped;
1258
+ continue;
1259
+ }
1260
+ if (/\s/.test(code[i])) {
1261
+ i++;
1262
+ continue;
1263
+ }
1264
+ break;
1265
+ }
1266
+ return i;
1267
+ }
1268
+ function transformRouter(code, filePath, routerFnNames, absolutePath, warn) {
1185
1269
  const pat = new RegExp(
1186
- `\\b(?:${routerFnNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\s*(?:<[^>]*>)?\\s*\\(`,
1270
+ `\\b(?:${routerFnNames.map(escapeRegExp).join("|")})\\b`,
1187
1271
  "g"
1188
1272
  );
1189
1273
  let match;
1190
1274
  const s = new MagicString2(code);
1191
1275
  let changed = false;
1276
+ const unsupportedSites = [];
1192
1277
  const basename2 = path3.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
1193
1278
  const routeNamesImport = `./${basename2}.named-routes.gen.js`;
1194
1279
  const routeNamesVar = `__rsc_rn`;
@@ -1197,23 +1282,35 @@ function transformRouter(code, filePath, routerFnNames, absolutePath) {
1197
1282
  while ((match = pat.exec(code)) !== null) {
1198
1283
  if (!codeOffsets.has(match.index)) continue;
1199
1284
  const callStart = match.index;
1200
- const parenPos = match.index + match[0].length - 1;
1285
+ const calleeEnd = match.index + match[0].length;
1286
+ const parenPos = findCallParenAfterGenerics(code, calleeEnd);
1287
+ if (parenPos === -1) continue;
1201
1288
  const closeParen = findMatchingParen(code, parenPos + 1);
1202
1289
  const callArgs = code.slice(parenPos + 1, closeParen);
1203
1290
  if (callArgs.includes("$$id")) continue;
1291
+ const sourceFilePath = absolutePath ?? filePath;
1204
1292
  const lineNumber = code.slice(0, callStart).split("\n").length;
1205
1293
  const hash = createHash("sha256").update(`${filePath}:${lineNumber}`).digest("hex").slice(0, 8);
1206
- changed = true;
1207
- const sourceFilePath = absolutePath ?? filePath;
1208
1294
  const injected = ` $$id: "${hash}", $$sourceFile: "${sourceFilePath}", $$routeNames: ${routeNamesVar},`;
1209
- const afterParen = callArgs.trimStart();
1210
- if (afterParen.startsWith("{")) {
1211
- const bracePos = code.indexOf("{", parenPos + 1);
1212
- s.appendRight(bracePos + 1, injected);
1213
- } else if (afterParen.startsWith(")")) {
1295
+ const argsContentStart = skipLeadingTrivia(code, parenPos + 1, closeParen);
1296
+ const firstArgChar = code[argsContentStart];
1297
+ if (firstArgChar === "{") {
1298
+ changed = true;
1299
+ s.appendRight(argsContentStart + 1, injected);
1300
+ } else if (argsContentStart >= closeParen - 1) {
1301
+ changed = true;
1214
1302
  s.appendRight(parenPos + 1, `{${injected} }`);
1303
+ } else {
1304
+ const lastNl = code.lastIndexOf("\n", callStart - 1);
1305
+ const column = callStart - (lastNl + 1) + 1;
1306
+ unsupportedSites.push({ line: lineNumber, column });
1215
1307
  }
1216
1308
  }
1309
+ if (unsupportedSites.length > 0 && warn) {
1310
+ warn(
1311
+ buildUnsupportedShapeWarning(filePath, "createRouter", unsupportedSites)
1312
+ );
1313
+ }
1217
1314
  if (!changed) return null;
1218
1315
  s.prepend(
1219
1316
  `import { NamedRoutes as ${routeNamesVar} } from "${routeNamesImport}";
@@ -1247,11 +1344,13 @@ function exposeRouterId() {
1247
1344
  try {
1248
1345
  const filePath = normalizePath(path3.relative(projectRoot, id));
1249
1346
  const routerFnNames = getImportedFnNames(code, "createRouter");
1347
+ const warn = typeof this.warn === "function" ? (message) => this.warn(message) : void 0;
1250
1348
  return transformRouter(
1251
1349
  code,
1252
1350
  filePath,
1253
1351
  routerFnNames,
1254
- normalizePath(id)
1352
+ normalizePath(id),
1353
+ warn
1255
1354
  );
1256
1355
  } finally {
1257
1356
  counter?.record(id, performance.now() - start);
@@ -2133,7 +2232,7 @@ import { resolve } from "node:path";
2133
2232
  // package.json
2134
2233
  var package_default = {
2135
2234
  name: "@rangojs/router",
2136
- version: "0.0.0-experimental.130",
2235
+ version: "0.0.0-experimental.132",
2137
2236
  description: "Django-inspired RSC router with composable URL patterns",
2138
2237
  keywords: [
2139
2238
  "react",
@@ -2296,7 +2395,7 @@ var package_default = {
2296
2395
  tag: "experimental"
2297
2396
  },
2298
2397
  scripts: {
2299
- build: "pnpm dlx 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 dlx esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm dlx 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",
2398
+ build: "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",
2300
2399
  prepublishOnly: "pnpm build",
2301
2400
  typecheck: "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
2302
2401
  test: "playwright test",
@@ -2311,7 +2410,7 @@ var package_default = {
2311
2410
  "@vitejs/plugin-rsc": "^0.5.26",
2312
2411
  debug: "^4.4.1",
2313
2412
  "magic-string": "^0.30.17",
2314
- picomatch: "^4.0.3",
2413
+ picomatch: "^4.0.4",
2315
2414
  "rsc-html-stream": "^0.0.7",
2316
2415
  tinyexec: "^0.3.2"
2317
2416
  },
@@ -2357,6 +2456,9 @@ var package_default = {
2357
2456
  vitest: {
2358
2457
  optional: true
2359
2458
  }
2459
+ },
2460
+ engines: {
2461
+ node: "^20.19.0 || >=22.12.0"
2360
2462
  }
2361
2463
  };
2362
2464
 
@@ -2459,8 +2561,8 @@ function extractObjectStringProperties(node) {
2459
2561
  }
2460
2562
 
2461
2563
  // src/build/route-types/ast-route-extraction.ts
2462
- function extractRoutesFromSource(code) {
2463
- const sourceFile = ts2.createSourceFile(
2564
+ function extractRoutesFromSource(code, sourceFileArg) {
2565
+ const sourceFile = sourceFileArg ?? ts2.createSourceFile(
2464
2566
  "input.tsx",
2465
2567
  code,
2466
2568
  ts2.ScriptTarget.Latest,
@@ -2586,6 +2688,33 @@ import ts4 from "typescript";
2586
2688
  import { readFileSync, existsSync as existsSync2 } from "node:fs";
2587
2689
  import { dirname, resolve as resolve2 } from "node:path";
2588
2690
  import ts3 from "typescript";
2691
+ function createScanMemo() {
2692
+ return { files: /* @__PURE__ */ new Map(), blockSourceFiles: /* @__PURE__ */ new Map() };
2693
+ }
2694
+ function parseBlock(memo, block) {
2695
+ if (memo) {
2696
+ const cached = memo.blockSourceFiles.get(block);
2697
+ if (cached) return cached;
2698
+ }
2699
+ const sf = ts3.createSourceFile(
2700
+ "input.tsx",
2701
+ block,
2702
+ ts3.ScriptTarget.Latest,
2703
+ true,
2704
+ ts3.ScriptKind.TSX
2705
+ );
2706
+ if (memo) memo.blockSourceFiles.set(block, sf);
2707
+ return sf;
2708
+ }
2709
+ function readSourceMemoized(memo, realPath) {
2710
+ if (memo) {
2711
+ const cached = memo.files.get(realPath);
2712
+ if (cached !== void 0) return cached;
2713
+ }
2714
+ const source = readFileSync(realPath, "utf-8");
2715
+ if (memo) memo.files.set(realPath, source);
2716
+ return source;
2717
+ }
2589
2718
  function extractNamePrefixFromInclude(node) {
2590
2719
  if (node.arguments.length >= 3) {
2591
2720
  const thirdArg = node.arguments[2];
@@ -2601,8 +2730,8 @@ function extractNamePrefixFromInclude(node) {
2601
2730
  }
2602
2731
  return null;
2603
2732
  }
2604
- function extractIncludesWithDiagnostics(code) {
2605
- const sourceFile = ts3.createSourceFile(
2733
+ function extractIncludesWithDiagnostics(code, sourceFileArg) {
2734
+ const sourceFile = sourceFileArg ?? ts3.createSourceFile(
2606
2735
  "input.tsx",
2607
2736
  code,
2608
2737
  ts3.ScriptTarget.Latest,
@@ -2693,8 +2822,8 @@ function resolveImportPath(importSpec, fromFile) {
2693
2822
  }
2694
2823
  return null;
2695
2824
  }
2696
- function extractUrlsBlockForVariable(code, varName) {
2697
- const sourceFile = ts3.createSourceFile(
2825
+ function extractUrlsBlockForVariable(code, varName, sourceFileArg) {
2826
+ const sourceFile = sourceFileArg ?? ts3.createSourceFile(
2698
2827
  "input.tsx",
2699
2828
  code,
2700
2829
  ts3.ScriptTarget.Latest,
@@ -2716,16 +2845,20 @@ function extractUrlsBlockForVariable(code, varName) {
2716
2845
  visit(sourceFile);
2717
2846
  return result;
2718
2847
  }
2719
- function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSchemasOut, diagnosticsOut) {
2848
+ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSchemasOut, diagnosticsOut, memo) {
2720
2849
  const routeMap = {};
2721
- const localRoutes = extractRoutesFromSource(block);
2850
+ const blockSourceFile = parseBlock(memo, block);
2851
+ const localRoutes = extractRoutesFromSource(block, blockSourceFile);
2722
2852
  for (const { name, pattern, search } of localRoutes) {
2723
2853
  routeMap[name] = pattern;
2724
2854
  if (search && searchSchemasOut) {
2725
2855
  searchSchemasOut[name] = search;
2726
2856
  }
2727
2857
  }
2728
- const { resolved: includes, unresolvable } = extractIncludesWithDiagnostics(block);
2858
+ const { resolved: includes, unresolvable } = extractIncludesWithDiagnostics(
2859
+ block,
2860
+ blockSourceFile
2861
+ );
2729
2862
  if (diagnosticsOut) {
2730
2863
  for (const entry of unresolvable) {
2731
2864
  diagnosticsOut.push({ ...entry, sourceFile: filePath });
@@ -2752,12 +2885,15 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
2752
2885
  targetFile,
2753
2886
  imported.exportedName,
2754
2887
  visited,
2755
- diagnosticsOut
2888
+ diagnosticsOut,
2889
+ void 0,
2890
+ memo
2756
2891
  );
2757
2892
  } else {
2758
2893
  const sameFileBlock = extractUrlsBlockForVariable(
2759
2894
  fullSource,
2760
- variableName
2895
+ variableName,
2896
+ parseBlock(memo, fullSource)
2761
2897
  );
2762
2898
  if (!sameFileBlock) {
2763
2899
  if (diagnosticsOut) {
@@ -2775,7 +2911,9 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
2775
2911
  filePath,
2776
2912
  variableName,
2777
2913
  visited,
2778
- diagnosticsOut
2914
+ diagnosticsOut,
2915
+ void 0,
2916
+ memo
2779
2917
  );
2780
2918
  }
2781
2919
  if (namePrefix === null) {
@@ -2799,8 +2937,9 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
2799
2937
  }
2800
2938
  return routeMap;
2801
2939
  }
2802
- function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock) {
2940
+ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock, memo) {
2803
2941
  visited = visited ?? /* @__PURE__ */ new Set();
2942
+ memo = memo ?? createScanMemo();
2804
2943
  const realPath = resolve2(filePath);
2805
2944
  const key = variableName ? `${realPath}:${variableName}` : realPath;
2806
2945
  if (visited.has(key)) {
@@ -2810,7 +2949,7 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
2810
2949
  visited.add(key);
2811
2950
  let source;
2812
2951
  try {
2813
- source = readFileSync(realPath, "utf-8");
2952
+ source = readSourceMemoized(memo, realPath);
2814
2953
  } catch {
2815
2954
  return { routes: {}, searchSchemas: {} };
2816
2955
  }
@@ -2818,7 +2957,11 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
2818
2957
  if (inlineBlock) {
2819
2958
  block = inlineBlock;
2820
2959
  } else if (variableName) {
2821
- const extracted = extractUrlsBlockForVariable(source, variableName);
2960
+ const extracted = extractUrlsBlockForVariable(
2961
+ source,
2962
+ variableName,
2963
+ parseBlock(memo, source)
2964
+ );
2822
2965
  if (!extracted) return { routes: {}, searchSchemas: {} };
2823
2966
  block = extracted;
2824
2967
  } else {
@@ -2831,7 +2974,8 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
2831
2974
  realPath,
2832
2975
  visited,
2833
2976
  searchSchemas,
2834
- diagnosticsOut
2977
+ diagnosticsOut,
2978
+ memo
2835
2979
  );
2836
2980
  visited.delete(key);
2837
2981
  return { routes, searchSchemas };
@@ -4437,9 +4581,6 @@ import {
4437
4581
  writeFileSync as writeFileSync2
4438
4582
  } from "node:fs";
4439
4583
  import { resolve as resolve5 } from "node:path";
4440
- function escapeRegExp2(str) {
4441
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4442
- }
4443
4584
  function encodePathParam(value) {
4444
4585
  return String(value).split("/").map((segment) => encodeURIComponent(segment)).join("/");
4445
4586
  }
@@ -4447,7 +4588,7 @@ function substituteRouteParams(pattern, params, encode = encodeURIComponent) {
4447
4588
  let result = pattern;
4448
4589
  let hadOmittedOptional = false;
4449
4590
  for (const [key, value] of Object.entries(params)) {
4450
- const escaped = escapeRegExp2(key);
4591
+ const escaped = escapeRegExp(key);
4451
4592
  if (value === "") {
4452
4593
  result = result.replace(
4453
4594
  new RegExp(`:${escaped}(\\([^)]*\\))?(?!\\?)`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.130",
3
+ "version": "0.0.0-experimental.132",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -167,7 +167,7 @@
167
167
  "@vitejs/plugin-rsc": "^0.5.26",
168
168
  "debug": "^4.4.1",
169
169
  "magic-string": "^0.30.17",
170
- "picomatch": "^4.0.3",
170
+ "picomatch": "^4.0.4",
171
171
  "rsc-html-stream": "^0.0.7",
172
172
  "tinyexec": "^0.3.2"
173
173
  },
@@ -214,8 +214,11 @@
214
214
  "optional": true
215
215
  }
216
216
  },
217
+ "engines": {
218
+ "node": "^20.19.0 || >=22.12.0"
219
+ },
217
220
  "scripts": {
218
- "build": "pnpm dlx 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 dlx esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm dlx 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",
221
+ "build": "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",
219
222
  "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
220
223
  "test": "playwright test",
221
224
  "test:ui": "playwright test --ui",