@rangojs/router 0.0.0-experimental.83052288 → 0.0.0-experimental.84

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 (61) hide show
  1. package/dist/vite/index.js +143 -10
  2. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  3. package/package.json +4 -4
  4. package/skills/handler-use/SKILL.md +362 -0
  5. package/skills/hooks/SKILL.md +24 -18
  6. package/skills/intercept/SKILL.md +20 -0
  7. package/skills/layout/SKILL.md +22 -0
  8. package/skills/middleware/SKILL.md +32 -3
  9. package/skills/migrate-nextjs/SKILL.md +560 -0
  10. package/skills/migrate-react-router/SKILL.md +765 -0
  11. package/skills/parallel/SKILL.md +59 -0
  12. package/skills/rango/SKILL.md +24 -22
  13. package/skills/response-routes/SKILL.md +8 -0
  14. package/skills/route/SKILL.md +24 -0
  15. package/skills/streams-and-websockets/SKILL.md +283 -0
  16. package/src/browser/navigation-bridge.ts +21 -2
  17. package/src/browser/navigation-client.ts +64 -13
  18. package/src/browser/partial-update.ts +14 -2
  19. package/src/browser/prefetch/cache.ts +113 -21
  20. package/src/browser/prefetch/fetch.ts +148 -16
  21. package/src/browser/prefetch/queue.ts +36 -5
  22. package/src/browser/react/Link.tsx +30 -2
  23. package/src/browser/react/NavigationProvider.tsx +6 -2
  24. package/src/browser/react/use-navigation.ts +22 -2
  25. package/src/browser/react/use-params.ts +11 -1
  26. package/src/browser/segment-reconciler.ts +36 -14
  27. package/src/build/route-trie.ts +50 -24
  28. package/src/client.tsx +82 -174
  29. package/src/index.ts +37 -9
  30. package/src/response-utils.ts +28 -0
  31. package/src/reverse.ts +4 -1
  32. package/src/route-definition/dsl-helpers.ts +159 -20
  33. package/src/route-definition/helpers-types.ts +57 -13
  34. package/src/route-types.ts +7 -0
  35. package/src/router/handler-context.ts +4 -1
  36. package/src/router/lazy-includes.ts +5 -5
  37. package/src/router/manifest.ts +22 -13
  38. package/src/router/match-middleware/cache-lookup.ts +2 -1
  39. package/src/router/match-result.ts +82 -4
  40. package/src/router/middleware.ts +14 -1
  41. package/src/router/segment-resolution/fresh.ts +5 -0
  42. package/src/router/segment-resolution/revalidation.ts +7 -1
  43. package/src/rsc/handler.ts +6 -3
  44. package/src/rsc/helpers.ts +69 -41
  45. package/src/rsc/progressive-enhancement.ts +2 -0
  46. package/src/rsc/response-route-handler.ts +11 -1
  47. package/src/rsc/rsc-rendering.ts +7 -0
  48. package/src/rsc/server-action.ts +2 -0
  49. package/src/segment-content-promise.ts +67 -0
  50. package/src/segment-loader-promise.ts +122 -0
  51. package/src/segment-system.tsx +11 -61
  52. package/src/server/context.ts +26 -3
  53. package/src/types/route-entry.ts +11 -0
  54. package/src/types/segments.ts +1 -1
  55. package/src/urls/include-helper.ts +24 -14
  56. package/src/urls/path-helper-types.ts +30 -4
  57. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  58. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  59. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  60. package/src/vite/router-discovery.ts +60 -1
  61. package/src/vite/utils/prerender-utils.ts +20 -6
@@ -1864,7 +1864,7 @@ import { resolve } from "node:path";
1864
1864
  // package.json
1865
1865
  var package_default = {
1866
1866
  name: "@rangojs/router",
1867
- version: "0.0.0-experimental.83052288",
1867
+ version: "0.0.0-experimental.84",
1868
1868
  description: "Django-inspired RSC router with composable URL patterns",
1869
1869
  keywords: [
1870
1870
  "react",
@@ -1997,7 +1997,7 @@ var package_default = {
1997
1997
  tag: "experimental"
1998
1998
  },
1999
1999
  scripts: {
2000
- build: "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.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",
2000
+ 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/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",
2001
2001
  prepublishOnly: "pnpm build",
2002
2002
  typecheck: "tsc --noEmit",
2003
2003
  test: "playwright test",
@@ -2006,7 +2006,7 @@ var package_default = {
2006
2006
  "test:unit:watch": "vitest"
2007
2007
  },
2008
2008
  dependencies: {
2009
- "@vitejs/plugin-rsc": "^0.5.19",
2009
+ "@vitejs/plugin-rsc": "^0.5.23",
2010
2010
  "magic-string": "^0.30.17",
2011
2011
  picomatch: "^4.0.3",
2012
2012
  "rsc-html-stream": "^0.0.7"
@@ -2026,7 +2026,7 @@ var package_default = {
2026
2026
  },
2027
2027
  peerDependencies: {
2028
2028
  "@cloudflare/vite-plugin": "^1.25.0",
2029
- "@vitejs/plugin-rsc": "^0.5.14",
2029
+ "@vitejs/plugin-rsc": "^0.5.23",
2030
2030
  react: "^18.0.0 || ^19.0.0",
2031
2031
  vite: "^7.3.0"
2032
2032
  },
@@ -3260,7 +3260,7 @@ function createCjsToEsmPlugin() {
3260
3260
  import { createServer as createViteServer } from "vite";
3261
3261
  import { resolve as resolve8 } from "node:path";
3262
3262
  import { readFileSync as readFileSync6 } from "node:fs";
3263
- import { createRequire } from "node:module";
3263
+ import { createRequire, register } from "node:module";
3264
3264
  import { pathToFileURL } from "node:url";
3265
3265
 
3266
3266
  // src/vite/plugins/virtual-stub-plugin.ts
@@ -3287,6 +3287,113 @@ function createVirtualStubPlugin() {
3287
3287
  };
3288
3288
  }
3289
3289
 
3290
+ // src/vite/plugins/cloudflare-protocol-stub.ts
3291
+ var VIRTUAL_PREFIX = "virtual:rango-cloudflare-stub-";
3292
+ var NULL_PREFIX = "\0" + VIRTUAL_PREFIX;
3293
+ var CF_PREFIX = "cloudflare:";
3294
+ var BUILD_ENV_GLOBAL_KEY = "__rango_build_env__";
3295
+ var SOURCE_EXT_RE = /\.[mc]?[jt]sx?$/;
3296
+ var IMPORT_NODE_TYPES = /* @__PURE__ */ new Set([
3297
+ "ImportDeclaration",
3298
+ "ImportExpression",
3299
+ "ExportNamedDeclaration",
3300
+ "ExportAllDeclaration"
3301
+ ]);
3302
+ var STUBS = {
3303
+ "cloudflare:workers": `
3304
+ export class DurableObject { constructor(_ctx, _env) {} }
3305
+ export class WorkerEntrypoint { constructor(_ctx, _env) {} }
3306
+ export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
3307
+ export class RpcTarget {}
3308
+ export const env = globalThis[${JSON.stringify(BUILD_ENV_GLOBAL_KEY)}] ?? {};
3309
+ export default {};
3310
+ `,
3311
+ "cloudflare:email": `
3312
+ export class EmailMessage { constructor(_from, _to, _raw) {} }
3313
+ export default {};
3314
+ `,
3315
+ "cloudflare:sockets": `
3316
+ export function connect() { return {}; }
3317
+ export default {};
3318
+ `,
3319
+ "cloudflare:workflows": `
3320
+ export class NonRetryableError extends Error {
3321
+ constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
3322
+ }
3323
+ export default {};
3324
+ `
3325
+ };
3326
+ var FALLBACK_STUB = `export default {};
3327
+ `;
3328
+ function createCloudflareProtocolStubPlugin() {
3329
+ return {
3330
+ name: "@rangojs/router:cloudflare-protocol-stub",
3331
+ transform(code, id) {
3332
+ const cleanId = id.split("?")[0] ?? id;
3333
+ if (!SOURCE_EXT_RE.test(cleanId)) return null;
3334
+ if (!code.includes(CF_PREFIX)) return null;
3335
+ let ast;
3336
+ try {
3337
+ ast = this.parse(code);
3338
+ } catch {
3339
+ return null;
3340
+ }
3341
+ const hits = [];
3342
+ walk(ast, (node) => {
3343
+ if (!IMPORT_NODE_TYPES.has(node.type)) return;
3344
+ const source = node.source;
3345
+ if (!source || source.type !== "Literal") return;
3346
+ if (typeof source.value !== "string") return;
3347
+ if (!source.value.startsWith(CF_PREFIX)) return;
3348
+ if (typeof source.start !== "number" || typeof source.end !== "number")
3349
+ return;
3350
+ hits.push({
3351
+ start: source.start,
3352
+ end: source.end,
3353
+ value: source.value
3354
+ });
3355
+ });
3356
+ if (hits.length === 0) return null;
3357
+ hits.sort((a, b) => b.start - a.start);
3358
+ let out = code;
3359
+ for (const hit of hits) {
3360
+ const submodule = hit.value.slice(CF_PREFIX.length);
3361
+ const quote = code[hit.start] === "'" ? "'" : '"';
3362
+ out = out.slice(0, hit.start) + quote + VIRTUAL_PREFIX + submodule + quote + out.slice(hit.end);
3363
+ }
3364
+ return { code: out, map: null };
3365
+ },
3366
+ resolveId(id) {
3367
+ if (id.startsWith(VIRTUAL_PREFIX)) {
3368
+ return "\0" + id;
3369
+ }
3370
+ return null;
3371
+ },
3372
+ load(id) {
3373
+ if (!id.startsWith(NULL_PREFIX)) return null;
3374
+ const submodule = id.slice(NULL_PREFIX.length);
3375
+ const specifier = CF_PREFIX + submodule;
3376
+ return STUBS[specifier] ?? FALLBACK_STUB;
3377
+ }
3378
+ };
3379
+ }
3380
+ function walk(node, visit) {
3381
+ if (!node || typeof node !== "object") return;
3382
+ if (Array.isArray(node)) {
3383
+ for (const child of node) walk(child, visit);
3384
+ return;
3385
+ }
3386
+ const n = node;
3387
+ if (typeof n.type !== "string") return;
3388
+ visit(n);
3389
+ for (const key in n) {
3390
+ if (key === "loc" || key === "start" || key === "end" || key === "range") {
3391
+ continue;
3392
+ }
3393
+ walk(n[key], visit);
3394
+ }
3395
+ }
3396
+
3290
3397
  // src/vite/plugins/client-ref-hashing.ts
3291
3398
  import { relative } from "node:path";
3292
3399
  import { createHash as createHash2 } from "node:crypto";
@@ -3580,11 +3687,19 @@ function substituteRouteParams(pattern, params, encode = encodeURIComponent) {
3580
3687
  let hadOmittedOptional = false;
3581
3688
  for (const [key, value] of Object.entries(params)) {
3582
3689
  const escaped = escapeRegExp2(key);
3583
- result = result.replace(
3584
- new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
3585
- encode(value)
3586
- );
3587
- result = result.replace(`*${key}`, encode(value));
3690
+ if (value === "") {
3691
+ result = result.replace(
3692
+ new RegExp(`:${escaped}(\\([^)]*\\))?(?!\\?)`),
3693
+ ""
3694
+ );
3695
+ result = result.replace(`*${key}`, "");
3696
+ } else {
3697
+ result = result.replace(
3698
+ new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
3699
+ encode(value)
3700
+ );
3701
+ result = result.replace(`*${key}`, encode(value));
3702
+ }
3588
3703
  }
3589
3704
  result = result.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\?/g, () => {
3590
3705
  hadOmittedOptional = true;
@@ -4674,7 +4789,22 @@ function postprocessBundle(state) {
4674
4789
  }
4675
4790
 
4676
4791
  // src/vite/router-discovery.ts
4792
+ var loaderHookRegistered = false;
4793
+ function ensureCloudflareProtocolLoaderRegistered() {
4794
+ if (loaderHookRegistered) return;
4795
+ loaderHookRegistered = true;
4796
+ try {
4797
+ register(
4798
+ new URL("./plugins/cloudflare-protocol-loader-hook.mjs", import.meta.url)
4799
+ );
4800
+ } catch (err) {
4801
+ console.warn(
4802
+ `[rsc-router] Could not register Node ESM loader hook for cloudflare:* imports (${err?.message ?? err}). Falling back to Vite transform only.`
4803
+ );
4804
+ }
4805
+ }
4677
4806
  async function createTempRscServer(state, options = {}) {
4807
+ ensureCloudflareProtocolLoaderRegistered();
4678
4808
  const { default: rsc } = await import("@vitejs/plugin-rsc");
4679
4809
  return createViteServer({
4680
4810
  root: state.projectRoot,
@@ -4697,6 +4827,7 @@ async function createTempRscServer(state, options = {}) {
4697
4827
  ...options.forceBuild ? [hashClientRefs(state.projectRoot)] : [],
4698
4828
  createVersionPlugin(),
4699
4829
  createVirtualStubPlugin(),
4830
+ createCloudflareProtocolStubPlugin(),
4700
4831
  // Dev prerender must use dev-mode IDs (path-based) to match the workerd
4701
4832
  // runtime. forceBuild produces hashed IDs for production bundle consistency.
4702
4833
  exposeInternalIds(options.forceBuild ? { forceBuild: true } : void 0),
@@ -4748,6 +4879,7 @@ async function acquireBuildEnv(s, command, mode) {
4748
4879
  if (!result) return false;
4749
4880
  s.resolvedBuildEnv = result.env;
4750
4881
  s.buildEnvDispose = result.dispose ?? null;
4882
+ globalThis[BUILD_ENV_GLOBAL_KEY] = result.env;
4751
4883
  return true;
4752
4884
  }
4753
4885
  async function releaseBuildEnv(s) {
@@ -4760,6 +4892,7 @@ async function releaseBuildEnv(s) {
4760
4892
  s.buildEnvDispose = null;
4761
4893
  }
4762
4894
  s.resolvedBuildEnv = void 0;
4895
+ delete globalThis[BUILD_ENV_GLOBAL_KEY];
4763
4896
  }
4764
4897
  function createRouterDiscoveryPlugin(entryPath, opts) {
4765
4898
  const s = createDiscoveryState(entryPath, opts);
@@ -0,0 +1,76 @@
1
+ // Node ESM loader hook that resolves `cloudflare:*` imports to the same
2
+ // stub ESM the Vite transform produces for rewritten specifiers.
3
+ //
4
+ // Why both? The Vite transform (cloudflare-protocol-stub.ts) catches
5
+ // imports in modules that flow through Vite's plugin pipeline — covers
6
+ // user source and any node_modules package Vite fetches and transforms.
7
+ // But Vite/Rollup externalize certain packages (e.g. `partyserver`,
8
+ // which has `import { DurableObject, env } from "cloudflare:workers"`
9
+ // at its top level, and similar "workerd-native" libraries). Externalized
10
+ // modules bypass the transform: Rollup hands their resolution to Node's
11
+ // native ESM loader, which rejects URL-scheme specifiers. This loader
12
+ // hook registers via `module.register()` from `createTempRscServer` and
13
+ // intercepts `cloudflare:*` at Node's resolve layer — before the default
14
+ // loader throws ERR_UNSUPPORTED_ESM_URL_SCHEME.
15
+ //
16
+ // Lifecycle: the hook runs in a dedicated worker thread (Node ESM loader
17
+ // architecture) with its own globalThis. It cannot see the main thread's
18
+ // `__rango_build_env__` bridge, so the `env` export here is always `{}`.
19
+ // That's fine in practice — externalized libraries don't typically touch
20
+ // `env` at module top level; they read it at request time in workerd
21
+ // where the real module exists. Build-time prerender handlers in user
22
+ // source DO read `env`, but they flow through the Vite transform (which
23
+ // does bridge `env` from `getPlatformProxy()`), not through this loader.
24
+ //
25
+ // Keep STUBS in sync with cloudflare-protocol-stub.ts — both paths need
26
+ // to hand out the same base classes.
27
+
28
+ const CF_PREFIX = "cloudflare:";
29
+
30
+ const STUBS = {
31
+ "cloudflare:workers": `
32
+ export class DurableObject { constructor(_ctx, _env) {} }
33
+ export class WorkerEntrypoint { constructor(_ctx, _env) {} }
34
+ export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
35
+ export class RpcTarget {}
36
+ export const env = {};
37
+ export default {};
38
+ `,
39
+ "cloudflare:email": `
40
+ export class EmailMessage { constructor(_from, _to, _raw) {} }
41
+ export default {};
42
+ `,
43
+ "cloudflare:sockets": `
44
+ export function connect() { return {}; }
45
+ export default {};
46
+ `,
47
+ "cloudflare:workflows": `
48
+ export class NonRetryableError extends Error {
49
+ constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
50
+ }
51
+ export default {};
52
+ `,
53
+ };
54
+
55
+ // Policy: unknown `cloudflare:*` specifiers resolve permissively to an
56
+ // empty default export rather than throwing. Same reasoning as
57
+ // cloudflare-protocol-stub.ts's FALLBACK_STUB — we prioritize
58
+ // dependency-graph resilience over strict validation, because third-party
59
+ // packages can pull `cloudflare:*` modules we haven't curated.
60
+ const FALLBACK_STUB = `export default {};\n`;
61
+
62
+ function dataUrlFor(specifier) {
63
+ const body = STUBS[specifier] ?? FALLBACK_STUB;
64
+ return "data:text/javascript;base64," + Buffer.from(body).toString("base64");
65
+ }
66
+
67
+ export async function resolve(specifier, context, nextResolve) {
68
+ if (specifier.startsWith(CF_PREFIX)) {
69
+ return {
70
+ shortCircuit: true,
71
+ url: dataUrlFor(specifier),
72
+ format: "module",
73
+ };
74
+ }
75
+ return nextResolve(specifier, context);
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.83052288",
3
+ "version": "0.0.0-experimental.84",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -133,7 +133,7 @@
133
133
  "tag": "experimental"
134
134
  },
135
135
  "scripts": {
136
- "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.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",
136
+ "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/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",
137
137
  "prepublishOnly": "pnpm build",
138
138
  "typecheck": "tsc --noEmit",
139
139
  "test": "playwright test",
@@ -142,7 +142,7 @@
142
142
  "test:unit:watch": "vitest"
143
143
  },
144
144
  "dependencies": {
145
- "@vitejs/plugin-rsc": "^0.5.19",
145
+ "@vitejs/plugin-rsc": "^0.5.23",
146
146
  "magic-string": "^0.30.17",
147
147
  "picomatch": "^4.0.3",
148
148
  "rsc-html-stream": "^0.0.7"
@@ -162,7 +162,7 @@
162
162
  },
163
163
  "peerDependencies": {
164
164
  "@cloudflare/vite-plugin": "^1.25.0",
165
- "@vitejs/plugin-rsc": "^0.5.14",
165
+ "@vitejs/plugin-rsc": "^0.5.23",
166
166
  "react": "^18.0.0 || ^19.0.0",
167
167
  "vite": "^7.3.0"
168
168
  },