@rangojs/router 0.0.0-experimental.ad1a365c → 0.0.0-experimental.b000c598
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/README.md +50 -20
- package/dist/vite/index.js +164 -13
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +3 -3
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +28 -20
- package/skills/links/SKILL.md +88 -16
- package/skills/loader/SKILL.md +35 -2
- package/skills/migrate-react-router/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +3 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/navigation-bridge.ts +51 -2
- package/src/browser/navigation-client.ts +33 -10
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +20 -1
- package/src/browser/prefetch/cache.ts +124 -26
- package/src/browser/prefetch/fetch.ts +114 -38
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +18 -13
- package/src/browser/react/NavigationProvider.tsx +50 -11
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +8 -1
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/types.ts +13 -0
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +3 -2
- package/src/route-definition/dsl-helpers.ts +16 -3
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/router/handler-context.ts +20 -3
- package/src/router/lazy-includes.ts +1 -1
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/manifest.ts +6 -5
- package/src/router/match-api.ts +3 -3
- package/src/router/middleware-types.ts +2 -22
- package/src/router/middleware.ts +32 -4
- package/src/router/pattern-matching.ts +60 -9
- package/src/router/trie-matching.ts +10 -4
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +8 -4
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/progressive-enhancement.ts +2 -0
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +7 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/server/request-context.ts +10 -42
- package/src/types/handler-context.ts +2 -34
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/urls/response-types.ts +2 -10
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/rango.ts +30 -9
- package/src/vite/router-discovery.ts +60 -1
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +33 -1
package/README.md
CHANGED
|
@@ -161,13 +161,18 @@ const urlpatterns = urls(({ path }) => [
|
|
|
161
161
|
]);
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
Use `reverse()` as the default way to link to routes:
|
|
164
|
+
Use `ctx.reverse()` from handler context as the default way to link to routes from server code:
|
|
165
165
|
|
|
166
166
|
```tsx
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
const ProductPage: Handler<"product"> = (ctx) => {
|
|
168
|
+
const url = ctx.reverse("product", { slug: "widget" }); // "/product/widget"
|
|
169
|
+
const searchUrl = ctx.reverse("search", undefined, { q: "rsc" }); // "/search?q=rsc"
|
|
170
|
+
return <Link to={url}>Widget</Link>;
|
|
171
|
+
};
|
|
169
172
|
```
|
|
170
173
|
|
|
174
|
+
`router.reverse()` (exported from the router module) is the same function without a handler context, useful in scripts or tests. In request code, prefer `ctx.reverse()` — it auto-fills mount params from the current match.
|
|
175
|
+
|
|
171
176
|
### Composable URL Modules
|
|
172
177
|
|
|
173
178
|
Local route names compose cleanly with `include(..., { name })`:
|
|
@@ -479,39 +484,64 @@ const urlpatterns = urls(({ path, loader }) => [
|
|
|
479
484
|
|
|
480
485
|
## Navigation & Links
|
|
481
486
|
|
|
482
|
-
### Named Routes with `reverse()` (Server
|
|
487
|
+
### Named Routes with `ctx.reverse()` (Server)
|
|
483
488
|
|
|
484
|
-
In server components, use `reverse()` to generate URLs by route name:
|
|
489
|
+
In server components and handlers, use `ctx.reverse()` to generate URLs by route name. This is the default — it is typed, auto-fills mount params from the current match, and resolves both local (`.name`) and absolute (`name.sub`) names:
|
|
485
490
|
|
|
486
491
|
```tsx
|
|
487
492
|
import { Link } from "@rangojs/router/client";
|
|
493
|
+
import type { Handler } from "@rangojs/router";
|
|
494
|
+
|
|
495
|
+
const BlogPostPage: Handler<"blogPost"> = (ctx) => {
|
|
496
|
+
const backUrl = ctx.reverse("blog");
|
|
497
|
+
return <Link to={backUrl}>Back to blog</Link>;
|
|
498
|
+
};
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
`reverse()` is type-safe — route names and required params are checked at compile time. Included routes use dotted names: `ctx.reverse("api.health")`.
|
|
502
|
+
|
|
503
|
+
For scripts, tests, or other code without a handler context, import the router-level `reverse`:
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
488
506
|
import { reverse } from "./router";
|
|
507
|
+
reverse("blogPost", { slug: "my-post" });
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Client Components
|
|
511
|
+
|
|
512
|
+
**`reverse()` is server-only.** It depends on the route manifest and handler context — neither is available in the browser bundle. Client components receive URLs as props, loader data, or server-action return values:
|
|
513
|
+
|
|
514
|
+
```tsx
|
|
515
|
+
// server
|
|
516
|
+
function BlogIndex(ctx: HandlerContext) {
|
|
517
|
+
return (
|
|
518
|
+
<Nav
|
|
519
|
+
home={ctx.reverse("home")}
|
|
520
|
+
post={ctx.reverse("blogPost", { slug: "my-post" })}
|
|
521
|
+
/>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
"use client";
|
|
528
|
+
import { Link } from "@rangojs/router/client";
|
|
489
529
|
|
|
490
|
-
function
|
|
530
|
+
export function Nav({ home, post }: { home: string; post: string }) {
|
|
491
531
|
return (
|
|
492
532
|
<nav>
|
|
493
|
-
<Link to={
|
|
494
|
-
<Link to={
|
|
495
|
-
<Link to={reverse("about")}>About</Link>
|
|
533
|
+
<Link to={home}>Home</Link>
|
|
534
|
+
<Link to={post}>My Post</Link>
|
|
496
535
|
</nav>
|
|
497
536
|
);
|
|
498
537
|
}
|
|
499
538
|
```
|
|
500
539
|
|
|
501
|
-
`
|
|
502
|
-
|
|
503
|
-
Handlers also have `ctx.reverse()` directly on the context:
|
|
504
|
-
|
|
505
|
-
```tsx
|
|
506
|
-
const BlogPostPage: Handler<"blogPost"> = (ctx) => {
|
|
507
|
-
const backUrl = ctx.reverse("blog");
|
|
508
|
-
return <Link to={backUrl}>Back to blog</Link>;
|
|
509
|
-
};
|
|
510
|
-
```
|
|
540
|
+
For client-side navigation to static paths (no named-route lookup), use `href()` — see below. For URLs tied to named routes, always generate on the server and pass the string in.
|
|
511
541
|
|
|
512
542
|
### `href()` for Path Validation (Client Components)
|
|
513
543
|
|
|
514
|
-
In client components, use `href()` for compile-time path validation:
|
|
544
|
+
In client components, use `href()` for compile-time path validation on static path strings:
|
|
515
545
|
|
|
516
546
|
```tsx
|
|
517
547
|
"use client";
|
package/dist/vite/index.js
CHANGED
|
@@ -1859,12 +1859,13 @@ function getVirtualVersionContent(version) {
|
|
|
1859
1859
|
|
|
1860
1860
|
// src/vite/utils/package-resolution.ts
|
|
1861
1861
|
import { existsSync } from "node:fs";
|
|
1862
|
+
import { createRequire } from "node:module";
|
|
1862
1863
|
import { resolve } from "node:path";
|
|
1863
1864
|
|
|
1864
1865
|
// package.json
|
|
1865
1866
|
var package_default = {
|
|
1866
1867
|
name: "@rangojs/router",
|
|
1867
|
-
version: "0.0.0-experimental.
|
|
1868
|
+
version: "0.0.0-experimental.b000c598",
|
|
1868
1869
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1869
1870
|
keywords: [
|
|
1870
1871
|
"react",
|
|
@@ -1997,9 +1998,9 @@ var package_default = {
|
|
|
1997
1998
|
tag: "experimental"
|
|
1998
1999
|
},
|
|
1999
2000
|
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",
|
|
2001
|
+
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
2002
|
prepublishOnly: "pnpm build",
|
|
2002
|
-
typecheck: "tsc --noEmit",
|
|
2003
|
+
typecheck: "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit",
|
|
2003
2004
|
test: "playwright test",
|
|
2004
2005
|
"test:ui": "playwright test --ui",
|
|
2005
2006
|
"test:unit": "vitest run",
|
|
@@ -2041,6 +2042,7 @@ var package_default = {
|
|
|
2041
2042
|
};
|
|
2042
2043
|
|
|
2043
2044
|
// src/vite/utils/package-resolution.ts
|
|
2045
|
+
var require2 = createRequire(import.meta.url);
|
|
2044
2046
|
var VIRTUAL_PACKAGE_NAME = "@rangojs/router";
|
|
2045
2047
|
function getPublishedPackageName() {
|
|
2046
2048
|
return package_default.name;
|
|
@@ -2081,6 +2083,20 @@ function getPackageAliases() {
|
|
|
2081
2083
|
}
|
|
2082
2084
|
return aliases;
|
|
2083
2085
|
}
|
|
2086
|
+
function getVendorAliases() {
|
|
2087
|
+
const specs = [
|
|
2088
|
+
"@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
|
|
2089
|
+
"@vitejs/plugin-rsc/vendor/react-server-dom/server.edge"
|
|
2090
|
+
];
|
|
2091
|
+
const aliases = {};
|
|
2092
|
+
for (const spec of specs) {
|
|
2093
|
+
try {
|
|
2094
|
+
aliases[spec] = require2.resolve(spec);
|
|
2095
|
+
} catch {
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
return aliases;
|
|
2099
|
+
}
|
|
2084
2100
|
|
|
2085
2101
|
// src/build/route-types/param-extraction.ts
|
|
2086
2102
|
function extractParamsFromPattern(pattern) {
|
|
@@ -3260,7 +3276,7 @@ function createCjsToEsmPlugin() {
|
|
|
3260
3276
|
import { createServer as createViteServer } from "vite";
|
|
3261
3277
|
import { resolve as resolve8 } from "node:path";
|
|
3262
3278
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3263
|
-
import { createRequire } from "node:module";
|
|
3279
|
+
import { createRequire as createRequire2, register } from "node:module";
|
|
3264
3280
|
import { pathToFileURL } from "node:url";
|
|
3265
3281
|
|
|
3266
3282
|
// src/vite/plugins/virtual-stub-plugin.ts
|
|
@@ -3287,6 +3303,113 @@ function createVirtualStubPlugin() {
|
|
|
3287
3303
|
};
|
|
3288
3304
|
}
|
|
3289
3305
|
|
|
3306
|
+
// src/vite/plugins/cloudflare-protocol-stub.ts
|
|
3307
|
+
var VIRTUAL_PREFIX = "virtual:rango-cloudflare-stub-";
|
|
3308
|
+
var NULL_PREFIX = "\0" + VIRTUAL_PREFIX;
|
|
3309
|
+
var CF_PREFIX = "cloudflare:";
|
|
3310
|
+
var BUILD_ENV_GLOBAL_KEY = "__rango_build_env__";
|
|
3311
|
+
var SOURCE_EXT_RE = /\.[mc]?[jt]sx?$/;
|
|
3312
|
+
var IMPORT_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
3313
|
+
"ImportDeclaration",
|
|
3314
|
+
"ImportExpression",
|
|
3315
|
+
"ExportNamedDeclaration",
|
|
3316
|
+
"ExportAllDeclaration"
|
|
3317
|
+
]);
|
|
3318
|
+
var STUBS = {
|
|
3319
|
+
"cloudflare:workers": `
|
|
3320
|
+
export class DurableObject { constructor(_ctx, _env) {} }
|
|
3321
|
+
export class WorkerEntrypoint { constructor(_ctx, _env) {} }
|
|
3322
|
+
export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
|
|
3323
|
+
export class RpcTarget {}
|
|
3324
|
+
export const env = globalThis[${JSON.stringify(BUILD_ENV_GLOBAL_KEY)}] ?? {};
|
|
3325
|
+
export default {};
|
|
3326
|
+
`,
|
|
3327
|
+
"cloudflare:email": `
|
|
3328
|
+
export class EmailMessage { constructor(_from, _to, _raw) {} }
|
|
3329
|
+
export default {};
|
|
3330
|
+
`,
|
|
3331
|
+
"cloudflare:sockets": `
|
|
3332
|
+
export function connect() { return {}; }
|
|
3333
|
+
export default {};
|
|
3334
|
+
`,
|
|
3335
|
+
"cloudflare:workflows": `
|
|
3336
|
+
export class NonRetryableError extends Error {
|
|
3337
|
+
constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
|
|
3338
|
+
}
|
|
3339
|
+
export default {};
|
|
3340
|
+
`
|
|
3341
|
+
};
|
|
3342
|
+
var FALLBACK_STUB = `export default {};
|
|
3343
|
+
`;
|
|
3344
|
+
function createCloudflareProtocolStubPlugin() {
|
|
3345
|
+
return {
|
|
3346
|
+
name: "@rangojs/router:cloudflare-protocol-stub",
|
|
3347
|
+
transform(code, id) {
|
|
3348
|
+
const cleanId = id.split("?")[0] ?? id;
|
|
3349
|
+
if (!SOURCE_EXT_RE.test(cleanId)) return null;
|
|
3350
|
+
if (!code.includes(CF_PREFIX)) return null;
|
|
3351
|
+
let ast;
|
|
3352
|
+
try {
|
|
3353
|
+
ast = this.parse(code);
|
|
3354
|
+
} catch {
|
|
3355
|
+
return null;
|
|
3356
|
+
}
|
|
3357
|
+
const hits = [];
|
|
3358
|
+
walk(ast, (node) => {
|
|
3359
|
+
if (!IMPORT_NODE_TYPES.has(node.type)) return;
|
|
3360
|
+
const source = node.source;
|
|
3361
|
+
if (!source || source.type !== "Literal") return;
|
|
3362
|
+
if (typeof source.value !== "string") return;
|
|
3363
|
+
if (!source.value.startsWith(CF_PREFIX)) return;
|
|
3364
|
+
if (typeof source.start !== "number" || typeof source.end !== "number")
|
|
3365
|
+
return;
|
|
3366
|
+
hits.push({
|
|
3367
|
+
start: source.start,
|
|
3368
|
+
end: source.end,
|
|
3369
|
+
value: source.value
|
|
3370
|
+
});
|
|
3371
|
+
});
|
|
3372
|
+
if (hits.length === 0) return null;
|
|
3373
|
+
hits.sort((a, b) => b.start - a.start);
|
|
3374
|
+
let out = code;
|
|
3375
|
+
for (const hit of hits) {
|
|
3376
|
+
const submodule = hit.value.slice(CF_PREFIX.length);
|
|
3377
|
+
const quote = code[hit.start] === "'" ? "'" : '"';
|
|
3378
|
+
out = out.slice(0, hit.start) + quote + VIRTUAL_PREFIX + submodule + quote + out.slice(hit.end);
|
|
3379
|
+
}
|
|
3380
|
+
return { code: out, map: null };
|
|
3381
|
+
},
|
|
3382
|
+
resolveId(id) {
|
|
3383
|
+
if (id.startsWith(VIRTUAL_PREFIX)) {
|
|
3384
|
+
return "\0" + id;
|
|
3385
|
+
}
|
|
3386
|
+
return null;
|
|
3387
|
+
},
|
|
3388
|
+
load(id) {
|
|
3389
|
+
if (!id.startsWith(NULL_PREFIX)) return null;
|
|
3390
|
+
const submodule = id.slice(NULL_PREFIX.length);
|
|
3391
|
+
const specifier = CF_PREFIX + submodule;
|
|
3392
|
+
return STUBS[specifier] ?? FALLBACK_STUB;
|
|
3393
|
+
}
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
function walk(node, visit) {
|
|
3397
|
+
if (!node || typeof node !== "object") return;
|
|
3398
|
+
if (Array.isArray(node)) {
|
|
3399
|
+
for (const child of node) walk(child, visit);
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
const n = node;
|
|
3403
|
+
if (typeof n.type !== "string") return;
|
|
3404
|
+
visit(n);
|
|
3405
|
+
for (const key in n) {
|
|
3406
|
+
if (key === "loc" || key === "start" || key === "end" || key === "range") {
|
|
3407
|
+
continue;
|
|
3408
|
+
}
|
|
3409
|
+
walk(n[key], visit);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3290
3413
|
// src/vite/plugins/client-ref-hashing.ts
|
|
3291
3414
|
import { relative } from "node:path";
|
|
3292
3415
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -4682,7 +4805,22 @@ function postprocessBundle(state) {
|
|
|
4682
4805
|
}
|
|
4683
4806
|
|
|
4684
4807
|
// src/vite/router-discovery.ts
|
|
4808
|
+
var loaderHookRegistered = false;
|
|
4809
|
+
function ensureCloudflareProtocolLoaderRegistered() {
|
|
4810
|
+
if (loaderHookRegistered) return;
|
|
4811
|
+
loaderHookRegistered = true;
|
|
4812
|
+
try {
|
|
4813
|
+
register(
|
|
4814
|
+
new URL("./plugins/cloudflare-protocol-loader-hook.mjs", import.meta.url)
|
|
4815
|
+
);
|
|
4816
|
+
} catch (err) {
|
|
4817
|
+
console.warn(
|
|
4818
|
+
`[rsc-router] Could not register Node ESM loader hook for cloudflare:* imports (${err?.message ?? err}). Falling back to Vite transform only.`
|
|
4819
|
+
);
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4685
4822
|
async function createTempRscServer(state, options = {}) {
|
|
4823
|
+
ensureCloudflareProtocolLoaderRegistered();
|
|
4686
4824
|
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
4687
4825
|
return createViteServer({
|
|
4688
4826
|
root: state.projectRoot,
|
|
@@ -4705,6 +4843,7 @@ async function createTempRscServer(state, options = {}) {
|
|
|
4705
4843
|
...options.forceBuild ? [hashClientRefs(state.projectRoot)] : [],
|
|
4706
4844
|
createVersionPlugin(),
|
|
4707
4845
|
createVirtualStubPlugin(),
|
|
4846
|
+
createCloudflareProtocolStubPlugin(),
|
|
4708
4847
|
// Dev prerender must use dev-mode IDs (path-based) to match the workerd
|
|
4709
4848
|
// runtime. forceBuild produces hashed IDs for production bundle consistency.
|
|
4710
4849
|
exposeInternalIds(options.forceBuild ? { forceBuild: true } : void 0),
|
|
@@ -4721,7 +4860,7 @@ async function resolveBuildEnv(option, factoryCtx) {
|
|
|
4721
4860
|
);
|
|
4722
4861
|
}
|
|
4723
4862
|
try {
|
|
4724
|
-
const userRequire =
|
|
4863
|
+
const userRequire = createRequire2(
|
|
4725
4864
|
resolve8(factoryCtx.root, "package.json")
|
|
4726
4865
|
);
|
|
4727
4866
|
const wranglerPath = userRequire.resolve("wrangler");
|
|
@@ -4756,6 +4895,7 @@ async function acquireBuildEnv(s, command, mode) {
|
|
|
4756
4895
|
if (!result) return false;
|
|
4757
4896
|
s.resolvedBuildEnv = result.env;
|
|
4758
4897
|
s.buildEnvDispose = result.dispose ?? null;
|
|
4898
|
+
globalThis[BUILD_ENV_GLOBAL_KEY] = result.env;
|
|
4759
4899
|
return true;
|
|
4760
4900
|
}
|
|
4761
4901
|
async function releaseBuildEnv(s) {
|
|
@@ -4768,6 +4908,7 @@ async function releaseBuildEnv(s) {
|
|
|
4768
4908
|
s.buildEnvDispose = null;
|
|
4769
4909
|
}
|
|
4770
4910
|
s.resolvedBuildEnv = void 0;
|
|
4911
|
+
delete globalThis[BUILD_ENV_GLOBAL_KEY];
|
|
4771
4912
|
}
|
|
4772
4913
|
function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
4773
4914
|
const s = createDiscoveryState(entryPath, opts);
|
|
@@ -5273,7 +5414,7 @@ async function rango(options) {
|
|
|
5273
5414
|
const preset = resolvedOptions.preset ?? "node";
|
|
5274
5415
|
const showBanner = resolvedOptions.banner ?? true;
|
|
5275
5416
|
const plugins = [];
|
|
5276
|
-
const rangoAliases = getPackageAliases();
|
|
5417
|
+
const rangoAliases = { ...getPackageAliases(), ...getVendorAliases() };
|
|
5277
5418
|
const excludeDeps = [
|
|
5278
5419
|
...getExcludeDeps(),
|
|
5279
5420
|
// The public browser entry re-exports the RSDW browser client.
|
|
@@ -5284,6 +5425,8 @@ async function rango(options) {
|
|
|
5284
5425
|
// cjs-to-esm transform can patch the real file.
|
|
5285
5426
|
"@vitejs/plugin-rsc/vendor/react-server-dom/client.browser"
|
|
5286
5427
|
];
|
|
5428
|
+
const pkg = getPublishedPackageName();
|
|
5429
|
+
const nested = (spec) => `${pkg} > ${spec}`;
|
|
5287
5430
|
const routerRef = { path: void 0 };
|
|
5288
5431
|
const prerenderEnabled = true;
|
|
5289
5432
|
if (preset === "cloudflare") {
|
|
@@ -5321,7 +5464,7 @@ async function rango(options) {
|
|
|
5321
5464
|
// Pre-bundle rsc-html-stream to prevent discovery during first request
|
|
5322
5465
|
// Exclude rsc-router modules to ensure same Context instance
|
|
5323
5466
|
optimizeDeps: {
|
|
5324
|
-
include: ["rsc-html-stream/client"],
|
|
5467
|
+
include: [nested("rsc-html-stream/client")],
|
|
5325
5468
|
exclude: excludeDeps,
|
|
5326
5469
|
esbuildOptions: sharedEsbuildOptions
|
|
5327
5470
|
}
|
|
@@ -5346,8 +5489,10 @@ async function rango(options) {
|
|
|
5346
5489
|
"react-dom/static.edge",
|
|
5347
5490
|
"react/jsx-runtime",
|
|
5348
5491
|
"react/jsx-dev-runtime",
|
|
5349
|
-
"rsc-html-stream/server",
|
|
5350
|
-
|
|
5492
|
+
nested("rsc-html-stream/server"),
|
|
5493
|
+
nested(
|
|
5494
|
+
"@vitejs/plugin-rsc/vendor/react-server-dom/client.edge"
|
|
5495
|
+
)
|
|
5351
5496
|
],
|
|
5352
5497
|
exclude: excludeDeps,
|
|
5353
5498
|
esbuildOptions: sharedEsbuildOptions
|
|
@@ -5362,7 +5507,9 @@ async function rango(options) {
|
|
|
5362
5507
|
"react",
|
|
5363
5508
|
"react/jsx-runtime",
|
|
5364
5509
|
"react/jsx-dev-runtime",
|
|
5365
|
-
|
|
5510
|
+
nested(
|
|
5511
|
+
"@vitejs/plugin-rsc/vendor/react-server-dom/server.edge"
|
|
5512
|
+
)
|
|
5366
5513
|
],
|
|
5367
5514
|
exclude: excludeDeps,
|
|
5368
5515
|
esbuildOptions: sharedEsbuildOptions
|
|
@@ -5443,7 +5590,7 @@ ${list}`);
|
|
|
5443
5590
|
"react-dom",
|
|
5444
5591
|
"react/jsx-runtime",
|
|
5445
5592
|
"react/jsx-dev-runtime",
|
|
5446
|
-
"rsc-html-stream/client"
|
|
5593
|
+
nested("rsc-html-stream/client")
|
|
5447
5594
|
],
|
|
5448
5595
|
exclude: excludeDeps,
|
|
5449
5596
|
esbuildOptions: sharedEsbuildOptions,
|
|
@@ -5460,7 +5607,9 @@ ${list}`);
|
|
|
5460
5607
|
"react-dom/static.edge",
|
|
5461
5608
|
"react/jsx-runtime",
|
|
5462
5609
|
"react/jsx-dev-runtime",
|
|
5463
|
-
|
|
5610
|
+
nested(
|
|
5611
|
+
"@vitejs/plugin-rsc/vendor/react-server-dom/client.edge"
|
|
5612
|
+
)
|
|
5464
5613
|
],
|
|
5465
5614
|
exclude: excludeDeps,
|
|
5466
5615
|
esbuildOptions: sharedEsbuildOptions
|
|
@@ -5473,7 +5622,9 @@ ${list}`);
|
|
|
5473
5622
|
"react",
|
|
5474
5623
|
"react/jsx-runtime",
|
|
5475
5624
|
"react/jsx-dev-runtime",
|
|
5476
|
-
|
|
5625
|
+
nested(
|
|
5626
|
+
"@vitejs/plugin-rsc/vendor/react-server-dom/server.edge"
|
|
5627
|
+
)
|
|
5477
5628
|
],
|
|
5478
5629
|
esbuildOptions: sharedEsbuildOptions
|
|
5479
5630
|
}
|
|
@@ -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.
|
|
3
|
+
"version": "0.0.0-experimental.b000c598",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -133,9 +133,9 @@
|
|
|
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
|
-
"typecheck": "tsc --noEmit",
|
|
138
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit",
|
|
139
139
|
"test": "playwright test",
|
|
140
140
|
"test:ui": "playwright test --ui",
|
|
141
141
|
"test:unit": "vitest run",
|
|
@@ -141,9 +141,11 @@ path("/dashboard", (ctx) => {
|
|
|
141
141
|
breadcrumb({ label: "Dashboard", href: "/dashboard" });
|
|
142
142
|
return <DashboardNav handle={Breadcrumbs} />;
|
|
143
143
|
});
|
|
144
|
+
```
|
|
144
145
|
|
|
146
|
+
```tsx
|
|
145
147
|
// Client component
|
|
146
|
-
|
|
148
|
+
"use client";
|
|
147
149
|
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
148
150
|
|
|
149
151
|
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -298,9 +298,11 @@ path("/dashboard", (ctx) => {
|
|
|
298
298
|
push({ label: "Dashboard", href: "/dashboard" });
|
|
299
299
|
return <DashboardNav handle={Breadcrumbs} />;
|
|
300
300
|
});
|
|
301
|
+
```
|
|
301
302
|
|
|
303
|
+
```tsx
|
|
302
304
|
// Client component — typeof infers the full Handle<T> type
|
|
303
|
-
|
|
305
|
+
"use client";
|
|
304
306
|
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
305
307
|
|
|
306
308
|
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
@@ -593,6 +595,12 @@ function ProductPage() {
|
|
|
593
595
|
return <h1>Product {params.productId}</h1>;
|
|
594
596
|
}
|
|
595
597
|
|
|
598
|
+
// Annotate the expected shape via a generic
|
|
599
|
+
function ProductPageTyped() {
|
|
600
|
+
const { productId } = useParams<{ productId: string }>();
|
|
601
|
+
return <h1>Product {productId}</h1>;
|
|
602
|
+
}
|
|
603
|
+
|
|
596
604
|
// With selector for performance (re-renders only when selected value changes)
|
|
597
605
|
function ProductId() {
|
|
598
606
|
const productId = useParams((p) => p.productId);
|
|
@@ -600,7 +608,7 @@ function ProductId() {
|
|
|
600
608
|
}
|
|
601
609
|
```
|
|
602
610
|
|
|
603
|
-
Returns merged params from all matched route segments. Updates on navigation commit (not during pending navigation).
|
|
611
|
+
Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit (not during pending navigation).
|
|
604
612
|
|
|
605
613
|
### usePathname()
|
|
606
614
|
|
|
@@ -681,24 +689,24 @@ function MountInfo() {
|
|
|
681
689
|
}
|
|
682
690
|
```
|
|
683
691
|
|
|
684
|
-
See `/links` for full URL generation guide
|
|
692
|
+
See `/links` for full URL generation guide. The default server API is `ctx.reverse()`; in client components, receive URLs as props, loader data, or server-action return values — `reverse()` is not available in the browser.
|
|
685
693
|
|
|
686
694
|
## Hook Summary
|
|
687
695
|
|
|
688
|
-
| Hook | Purpose | Returns
|
|
689
|
-
| -------------------- | --------------------------------- |
|
|
690
|
-
| `useParams()` | Route params | `Record<string, string>` or selected value
|
|
691
|
-
| `usePathname()` | Current pathname | `string`
|
|
692
|
-
| `useSearchParams()` | URL search params | `ReadonlyURLSearchParams`
|
|
693
|
-
| `useHref()` | Mount-aware href | `(path) => string`
|
|
694
|
-
| `useMount()` | Current include() mount path | `string`
|
|
695
|
-
| `useNavigation()` | Reactive navigation state | state, location, isStreaming
|
|
696
|
-
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward
|
|
697
|
-
| `useSegments()` | URL path & segment IDs | path, segmentIds, location
|
|
698
|
-
| `useLinkStatus()` | Link pending state | { pending }
|
|
699
|
-
| `useLoader()` | Loader data (strict) | data, isLoading, error
|
|
700
|
-
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading
|
|
701
|
-
| `useHandle()` | Accumulated handle data | T (handle type)
|
|
702
|
-
| `useAction()` | Server action state | state, error, result
|
|
703
|
-
| `useLocationState()` | History state (persists or flash) | T \| undefined
|
|
704
|
-
| `useClientCache()` | Cache control | { clear }
|
|
696
|
+
| Hook | Purpose | Returns |
|
|
697
|
+
| -------------------- | --------------------------------- | ------------------------------------------------------------------ |
|
|
698
|
+
| `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
|
|
699
|
+
| `usePathname()` | Current pathname | `string` |
|
|
700
|
+
| `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
|
|
701
|
+
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
702
|
+
| `useMount()` | Current include() mount path | `string` |
|
|
703
|
+
| `useNavigation()` | Reactive navigation state | state, location, isStreaming |
|
|
704
|
+
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
|
|
705
|
+
| `useSegments()` | URL path & segment IDs | path, segmentIds, location |
|
|
706
|
+
| `useLinkStatus()` | Link pending state | { pending } |
|
|
707
|
+
| `useLoader()` | Loader data (strict) | data, isLoading, error |
|
|
708
|
+
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
|
|
709
|
+
| `useHandle()` | Accumulated handle data | T (handle type) |
|
|
710
|
+
| `useAction()` | Server action state | state, error, result |
|
|
711
|
+
| `useLocationState()` | History state (persists or flash) | T \| undefined |
|
|
712
|
+
| `useClientCache()` | Cache control | { clear } |
|