@rangojs/router 0.0.0-experimental.ede38110 → 0.0.0-experimental.f2d1a2f1

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 (66) hide show
  1. package/README.md +50 -20
  2. package/dist/vite/index.js +353 -49
  3. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  4. package/package.json +5 -3
  5. package/skills/breadcrumbs/SKILL.md +3 -1
  6. package/skills/hooks/SKILL.md +28 -20
  7. package/skills/links/SKILL.md +88 -16
  8. package/skills/loader/SKILL.md +35 -2
  9. package/skills/migrate-react-router/SKILL.md +1 -0
  10. package/skills/response-routes/SKILL.md +8 -0
  11. package/skills/streams-and-websockets/SKILL.md +283 -0
  12. package/skills/typesafety/SKILL.md +3 -1
  13. package/src/browser/app-shell.ts +52 -0
  14. package/src/browser/navigation-bridge.ts +51 -2
  15. package/src/browser/navigation-client.ts +33 -10
  16. package/src/browser/navigation-store.ts +25 -1
  17. package/src/browser/partial-update.ts +20 -1
  18. package/src/browser/prefetch/cache.ts +124 -26
  19. package/src/browser/prefetch/fetch.ts +114 -38
  20. package/src/browser/prefetch/queue.ts +36 -5
  21. package/src/browser/rango-state.ts +53 -13
  22. package/src/browser/react/Link.tsx +18 -13
  23. package/src/browser/react/NavigationProvider.tsx +50 -11
  24. package/src/browser/react/use-navigation.ts +30 -11
  25. package/src/browser/react/use-params.ts +11 -1
  26. package/src/browser/react/use-router.ts +8 -1
  27. package/src/browser/rsc-router.tsx +34 -6
  28. package/src/browser/types.ts +13 -0
  29. package/src/cache/cf/cf-cache-store.ts +5 -7
  30. package/src/index.rsc.ts +3 -0
  31. package/src/index.ts +3 -0
  32. package/src/outlet-context.ts +1 -1
  33. package/src/response-utils.ts +28 -0
  34. package/src/reverse.ts +3 -2
  35. package/src/route-definition/dsl-helpers.ts +16 -3
  36. package/src/route-definition/resolve-handler-use.ts +6 -0
  37. package/src/router/handler-context.ts +20 -3
  38. package/src/router/lazy-includes.ts +1 -1
  39. package/src/router/loader-resolution.ts +3 -0
  40. package/src/router/match-api.ts +3 -3
  41. package/src/router/middleware-types.ts +2 -22
  42. package/src/router/middleware.ts +32 -4
  43. package/src/router/pattern-matching.ts +60 -9
  44. package/src/router/trie-matching.ts +10 -4
  45. package/src/router/url-params.ts +49 -0
  46. package/src/router.ts +1 -2
  47. package/src/rsc/handler.ts +8 -4
  48. package/src/rsc/helpers.ts +69 -41
  49. package/src/rsc/progressive-enhancement.ts +2 -0
  50. package/src/rsc/response-route-handler.ts +14 -1
  51. package/src/rsc/rsc-rendering.ts +7 -0
  52. package/src/rsc/server-action.ts +2 -0
  53. package/src/server/request-context.ts +10 -42
  54. package/src/types/handler-context.ts +2 -34
  55. package/src/types/loader-types.ts +5 -6
  56. package/src/types/request-scope.ts +126 -0
  57. package/src/urls/response-types.ts +2 -10
  58. package/src/vite/debug.ts +86 -0
  59. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  60. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  61. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  62. package/src/vite/plugins/performance-tracks.ts +4 -6
  63. package/src/vite/rango.ts +49 -14
  64. package/src/vite/router-discovery.ts +161 -23
  65. package/src/vite/utils/banner.ts +1 -1
  66. package/src/vite/utils/package-resolution.ts +41 -1
@@ -37,6 +37,8 @@ import { track, type MetricsStore } from "./context.js";
37
37
  import { getFetchableLoader } from "./fetchable-loader-store.js";
38
38
  import type { SegmentCacheStore } from "../cache/types.js";
39
39
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
40
+ import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
41
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
40
42
  import { THEME_COOKIE } from "../theme/constants.js";
41
43
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
42
44
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
@@ -58,22 +60,7 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
58
60
  export interface RequestContext<
59
61
  TEnv = DefaultEnv,
60
62
  TParams = Record<string, string>,
61
- > {
62
- /** Platform bindings (Cloudflare env, etc.) */
63
- env: TEnv;
64
- /** Original HTTP request */
65
- request: Request;
66
- /** Parsed URL (with internal `_rsc*` params stripped) */
67
- url: URL;
68
- /**
69
- * The original request URL with all parameters intact, including
70
- * internal `_rsc*` transport params.
71
- */
72
- originalUrl: URL;
73
- /** URL pathname */
74
- pathname: string;
75
- /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
76
- searchParams: URLSearchParams;
63
+ > extends RequestScope<TEnv> {
77
64
  /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
78
65
  _variables: Record<string, any>;
79
66
  /** Get a variable set by middleware */
@@ -159,20 +146,6 @@ export interface RequestContext<
159
146
  import("../cache/profile-registry.js").CacheProfile
160
147
  >;
161
148
 
162
- /**
163
- * Schedule work to run after the response is sent.
164
- * On Cloudflare Workers, uses ctx.waitUntil().
165
- * On Node.js, runs as fire-and-forget.
166
- *
167
- * @example
168
- * ```typescript
169
- * ctx.waitUntil(async () => {
170
- * await cacheStore.set(key, data, ttl);
171
- * });
172
- * ```
173
- */
174
- waitUntil(fn: () => Promise<void>): void;
175
-
176
149
  /**
177
150
  * Register a callback to run when the response is created.
178
151
  * Callbacks are sync and receive the response. They can:
@@ -498,13 +471,7 @@ export function requireRequestContext<
498
471
  return getRequestContext<TEnv>();
499
472
  }
500
473
 
501
- /**
502
- * Cloudflare Workers ExecutionContext (subset we need)
503
- */
504
- export interface ExecutionContext {
505
- waitUntil(promise: Promise<any>): void;
506
- passThroughOnException(): void;
507
- }
474
+ export type { ExecutionContext };
508
475
 
509
476
  /**
510
477
  * Options for creating a request context
@@ -768,16 +735,14 @@ export function createRequestContext<TEnv>(
768
735
 
769
736
  waitUntil(fn: () => Promise<void>): void {
770
737
  if (executionContext?.waitUntil) {
771
- // Cloudflare Workers: use native waitUntil
772
738
  executionContext.waitUntil(fn());
773
739
  } else {
774
- // Node.js / dev: fire-and-forget with error logging
775
- fn().catch((err) =>
776
- console.error("[waitUntil] Background task failed:", err),
777
- );
740
+ fireAndForgetWaitUntil(fn);
778
741
  }
779
742
  },
780
743
 
744
+ executionContext,
745
+
781
746
  _onResponseCallbacks: [],
782
747
 
783
748
  onResponse(callback: (response: Response) => Response): void {
@@ -1043,7 +1008,10 @@ export function createUseFunction<TEnv>(
1043
1008
  search: (ctx as any).search ?? {},
1044
1009
  pathname: ctx.pathname,
1045
1010
  url: ctx.url,
1011
+ originalUrl: ctx.originalUrl,
1046
1012
  env: ctx.env as any,
1013
+ waitUntil: ctx.waitUntil.bind(ctx),
1014
+ executionContext: ctx.executionContext,
1047
1015
  get: ctx.get as any,
1048
1016
  use: (<TDep, TDepParams = any>(
1049
1017
  dep: LoaderDefinition<TDep, TDepParams>,
@@ -20,6 +20,7 @@ import type {
20
20
  } from "./route-config.js";
21
21
  import type { LoaderDefinition } from "./loader-types.js";
22
22
  import type { UseItems, HandlerUseItem } from "../route-types.js";
23
+ import type { RequestScope } from "./request-scope.js";
23
24
 
24
25
  // Re-export MiddlewareFn for internal/advanced use
25
26
  export type { MiddlewareFn } from "../router/middleware.js";
@@ -195,7 +196,7 @@ export type HandlerContext<
195
196
  TEnv = DefaultEnv,
196
197
  TSearch extends SearchSchema = {},
197
198
  TRouteMap = never,
198
- > = {
199
+ > = RequestScope<TEnv> & {
199
200
  /**
200
201
  * Route parameters extracted from the URL pattern.
201
202
  * Type-safe when using Handler<"/path/:param"> or Handler<{ param: string }>.
@@ -215,44 +216,11 @@ export type HandlerContext<
215
216
  * changing build semantics (e.g., skip expensive operations in dev).
216
217
  */
217
218
  dev: boolean;
218
- /**
219
- * The original incoming Request object (transport URL intact).
220
- * Use `ctx.url` / `ctx.searchParams` for application logic — those have
221
- * internal `_rsc*` params stripped. `ctx.request` preserves the raw URL
222
- * for cases where you need original headers, method, or body.
223
- */
224
- request: Request;
225
- /**
226
- * Query parameters from the URL (system params like `_rsc*` are filtered).
227
- * Always a standard URLSearchParams instance.
228
- */
229
- searchParams: URLSearchParams;
230
219
  /**
231
220
  * Typed search parameters parsed from URL query string via the route's
232
221
  * search schema. Empty object when no schema is defined.
233
222
  */
234
223
  search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
235
- /**
236
- * The pathname portion of the request URL.
237
- */
238
- pathname: string;
239
- /**
240
- * The full URL object (with internal `_rsc*` params stripped).
241
- * Use this for application logic — routing, link generation, display.
242
- */
243
- url: URL;
244
- /**
245
- * The original request URL with all parameters intact, including
246
- * internal `_rsc*` transport params. Use `ctx.url` for application
247
- * logic — this is only needed for advanced cases like debugging
248
- * or custom cache keying.
249
- */
250
- originalUrl: URL;
251
- /**
252
- * Platform bindings (DB, KV, secrets, etc.).
253
- * Access resources like `ctx.env.DB`, `ctx.env.KV`.
254
- */
255
- env: TEnv;
256
224
  /**
257
225
  * Type-safe getter for middleware variables.
258
226
  * Preferred way to read middleware-injected variables.
@@ -3,11 +3,13 @@ import type { Handle } from "../handle.js";
3
3
  import type { MiddlewareFn } from "../router/middleware.js";
4
4
  import type { ScopedReverseFunction } from "../reverse.js";
5
5
  import type { SearchSchema, ResolveSearchSchema } from "../search-params.js";
6
+ import type { UseItems, LoaderUseItem } from "../route-types.js";
6
7
  import type {
7
8
  DefaultEnv,
8
9
  DefaultReverseRouteMap,
9
10
  DefaultVars,
10
11
  } from "./global-namespace.js";
12
+ import type { RequestScope } from "./request-scope.js";
11
13
 
12
14
  /**
13
15
  * Context passed to loader functions during execution
@@ -39,7 +41,7 @@ export type LoaderContext<
39
41
  TEnv = DefaultEnv,
40
42
  TBody = unknown,
41
43
  TSearch extends SearchSchema = {},
42
- > = {
44
+ > = RequestScope<TEnv> & {
43
45
  params: TParams;
44
46
  /**
45
47
  * Route params extracted from the URL pattern match (server-side only).
@@ -48,12 +50,7 @@ export type LoaderContext<
48
50
  * resource scoping.
49
51
  */
50
52
  routeParams: Record<string, string>;
51
- request: Request;
52
- searchParams: URLSearchParams;
53
53
  search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
54
- pathname: string;
55
- url: URL;
56
- env: TEnv;
57
54
  get: {
58
55
  <T>(contextVar: ContextVar<T>): T | undefined;
59
56
  } & (<K extends keyof DefaultVars>(key: K) => DefaultVars[K]);
@@ -207,4 +204,6 @@ export type LoaderDefinition<
207
204
  __brand: "loader";
208
205
  $$id: string; // Injected by Vite plugin (exposeInternalIds) - unique identifier
209
206
  fn?: LoaderFn<T, TParams, any>; // Optional - server-side only, stored in registry for RSC
207
+ /** Composable default DSL items merged when the loader is mounted. */
208
+ use?: () => UseItems<LoaderUseItem>;
210
209
  };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * RequestScope: the fields every user-facing context shares.
3
+ *
4
+ * A handler, middleware, loader, response handler, and the ALS-bound
5
+ * RequestContext are all different phases of the same request, and they
6
+ * all carry the same set of request-scoped capabilities: the raw Request,
7
+ * the parsed URL pair (`url` is cleaned of internal `_rsc*` params,
8
+ * `originalUrl` retains them), pathname/searchParams, platform bindings
9
+ * (`env`), and two escape hatches for work that outlives the response
10
+ * (`waitUntil`) or needs the raw Cloudflare runtime object
11
+ * (`executionContext`).
12
+ *
13
+ * Each public context type intersects `RequestScope<TEnv>` with its own
14
+ * phase-specific fields (e.g. `params`/`reverse` on HandlerContext,
15
+ * `headers`/`header()` on MiddlewareContext). That keeps platform surface
16
+ * in one place and lets the next runtime escape hatch we need land in
17
+ * one file instead of four.
18
+ */
19
+
20
+ import type { DefaultEnv } from "./global-namespace.js";
21
+
22
+ /**
23
+ * Minimal subset of Cloudflare Workers' ExecutionContext that the router
24
+ * uses. Defined locally so the package does not depend on
25
+ * `@cloudflare/workers-types`. Consumers that want the full type can cast.
26
+ *
27
+ * On non-Cloudflare runtimes (Node, dev server, tests), this is undefined
28
+ * — portable apps should prefer `ctx.waitUntil(...)`, which degrades
29
+ * gracefully. `ctx.executionContext` is the escape hatch for libraries
30
+ * (MCP, Durable Object routing, etc.) that type their arguments as the
31
+ * raw ExecutionContext.
32
+ */
33
+ export interface ExecutionContext {
34
+ waitUntil(promise: Promise<any>): void;
35
+ passThroughOnException(): void;
36
+ }
37
+
38
+ /**
39
+ * Fallback `waitUntil` body used when no Cloudflare `ExecutionContext`
40
+ * is available (Node, dev, tests). Runs the work fire-and-forget and
41
+ * logs errors so they don't silently swallow.
42
+ *
43
+ * Exported so every `waitUntil` call site degrades identically instead
44
+ * of inventing its own fallback policy.
45
+ */
46
+ export function fireAndForgetWaitUntil(fn: () => Promise<void>): void {
47
+ fn().catch((err) =>
48
+ console.error("[waitUntil] Background task failed:", err),
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Fields present on every user-facing request context.
54
+ *
55
+ * @template TEnv - Platform bindings type (Cloudflare env, etc.).
56
+ */
57
+ export interface RequestScope<TEnv = DefaultEnv> {
58
+ /**
59
+ * The original incoming Request object (transport URL intact).
60
+ * Use `url` / `searchParams` for application logic — those have
61
+ * internal `_rsc*` params stripped. `request` preserves the raw URL
62
+ * when you need original headers, method, or body.
63
+ */
64
+ request: Request;
65
+
66
+ /**
67
+ * The request URL with internal `_rsc*` transport params stripped.
68
+ * Use this for routing, link generation, and display.
69
+ */
70
+ url: URL;
71
+
72
+ /**
73
+ * The original request URL with all parameters intact, including
74
+ * internal `_rsc*` transport params. Use `url` for application logic
75
+ * — this is only needed for advanced cases like debugging or custom
76
+ * cache keying.
77
+ */
78
+ originalUrl: URL;
79
+
80
+ /** URL pathname (same as `url.pathname`). */
81
+ pathname: string;
82
+
83
+ /**
84
+ * Query parameters from the URL (system params like `_rsc*` are
85
+ * filtered). Always a standard `URLSearchParams` instance.
86
+ */
87
+ searchParams: URLSearchParams;
88
+
89
+ /**
90
+ * Platform bindings (DB, KV, secrets, etc.). On Cloudflare Workers
91
+ * these are the `env` object passed to the Worker's `fetch()` handler.
92
+ */
93
+ env: TEnv;
94
+
95
+ /**
96
+ * Schedule work to run after the response is sent.
97
+ * On Cloudflare Workers, delegates to `executionContext.waitUntil()`.
98
+ * On Node / dev / tests, runs as fire-and-forget with error logging.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * ctx.waitUntil(async () => {
103
+ * await cacheStore.set(key, data, ttl);
104
+ * });
105
+ * ```
106
+ */
107
+ waitUntil(fn: () => Promise<void>): void;
108
+
109
+ /**
110
+ * Raw Cloudflare Workers `ExecutionContext`, when running on a
111
+ * Cloudflare-compatible runtime. Undefined elsewhere.
112
+ *
113
+ * Escape hatch for libraries that type their arguments as
114
+ * `ExecutionContext` (MCP `fetch`, `routeAgentRequest`, etc.).
115
+ * For the common "do work after the response" case, prefer
116
+ * `ctx.waitUntil(...)` — it is platform-neutral.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * path.any("/mcp", (ctx) =>
121
+ * emailMcp.fetch(ctx.request, ctx.env, ctx.executionContext!),
122
+ * );
123
+ * ```
124
+ */
125
+ executionContext?: ExecutionContext;
126
+ }
@@ -5,6 +5,7 @@ import type {
5
5
  DefaultVars,
6
6
  } from "../types/global-namespace.js";
7
7
  import type { UseItems, ResponseRouteUseItem } from "../route-types.js";
8
+ import type { RequestScope } from "../types/request-scope.js";
8
9
 
9
10
  /**
10
11
  * Reverse function for response handler contexts.
@@ -93,19 +94,10 @@ export type TextResponseHandler<
93
94
  export interface ResponseHandlerContext<
94
95
  TParams = Record<string, string>,
95
96
  TEnv = any,
96
- > {
97
- request: Request;
97
+ > extends RequestScope<TEnv> {
98
98
  params: TParams;
99
99
  /** @internal Phantom property for params type invariance. Prevents mounting handlers on wrong routes. */
100
100
  readonly _paramCheck?: (params: TParams) => TParams;
101
- /** Platform bindings (DB, KV, secrets, etc.). */
102
- env: TEnv;
103
- /** Query parameters from the URL (system params like `_rsc*` are filtered). */
104
- searchParams: URLSearchParams;
105
- /** The full URL object (with system params filtered). */
106
- url: URL;
107
- /** The pathname portion of the request URL. */
108
- pathname: string;
109
101
  reverse: ResponseReverseFunction;
110
102
  /** Read a variable set by middleware via ctx.set(key, value) or ctx.set(ContextVar, value). */
111
103
  get: {
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Debug logging for the Rango Vite plugin.
3
+ *
4
+ * Thin wrapper over the `debug` package (the same one Vite uses for its
5
+ * own `vite:*` namespaces). Enable with either:
6
+ *
7
+ * DEBUG='rango:*' vite dev
8
+ * vite --debug rango:* # vite prepends `vite:`, we bridge it
9
+ *
10
+ * Returns `undefined` when no matching namespace is enabled, so call sites
11
+ * can guard expensive diagnostics with a simple truthiness check:
12
+ *
13
+ * const debug = createRangoDebugger("rango:routes");
14
+ * if (debug) debug("built manifest (%d routes) in %dms", n, ms);
15
+ *
16
+ * Back-compat: INTERNAL_RANGO_DEBUG=1 still enables all rango namespaces.
17
+ *
18
+ * Vite CLI note: `vite --debug <feat>` rewrites to `DEBUG=vite:<feat>` — it
19
+ * always prefixes with `vite:` and cannot enable bare `rango:*` namespaces.
20
+ * We work around this by registering a shadow `vite:rango:*` instance for
21
+ * each debugger, so either invocation works.
22
+ */
23
+
24
+ import debugFactory from "debug";
25
+
26
+ // Back-compat: the legacy INTERNAL_RANGO_DEBUG env var enabled per-site
27
+ // console.logs in this plugin. Map it to `rango:*` so those call sites can
28
+ // be migrated to the `debug` pipeline without breaking existing setups.
29
+ // Uses debug.enable() rather than mutating process.env because the `debug`
30
+ // package already snapshotted DEBUG when it was imported above.
31
+ if (process.env.INTERNAL_RANGO_DEBUG) {
32
+ const existing = debugFactory.disable();
33
+ debugFactory.enable(existing ? `${existing},rango:*` : "rango:*");
34
+ }
35
+
36
+ export type Debugger = (formatter: string, ...args: unknown[]) => void;
37
+
38
+ export function createRangoDebugger(namespace: string): Debugger | undefined {
39
+ const primary = debugFactory(namespace);
40
+ // Shadow namespace so `vite --debug rango:*` (which expands to
41
+ // DEBUG=vite:rango:*) and `vite --debug` (DEBUG=vite:*) both pick us up.
42
+ const shadow = debugFactory(`vite:${namespace}`);
43
+ if (primary.enabled) return primary as unknown as Debugger;
44
+ if (shadow.enabled) return shadow as unknown as Debugger;
45
+ return undefined;
46
+ }
47
+
48
+ /**
49
+ * Measure an async block and log its duration via `debug`. No-ops (still
50
+ * runs `fn`) when the namespace is disabled, so production cost is a single
51
+ * `.enabled` check per call.
52
+ *
53
+ * await timed(debug, "discover routers", () => discoverRouters(state));
54
+ */
55
+ export async function timed<T>(
56
+ debug: Debugger | undefined,
57
+ label: string,
58
+ fn: () => T | Promise<T>,
59
+ ): Promise<T> {
60
+ if (!debug) return await fn();
61
+ const start = performance.now();
62
+ try {
63
+ return await fn();
64
+ } finally {
65
+ debug("%s (%sms)", label, (performance.now() - start).toFixed(1));
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Synchronous variant of `timed`. Use for sync call sites — wrapping them
71
+ * with the async `timed` would create a floating promise that discards any
72
+ * throw, bypassing the surrounding try/catch.
73
+ */
74
+ export function timedSync<T>(
75
+ debug: Debugger | undefined,
76
+ label: string,
77
+ fn: () => T,
78
+ ): T {
79
+ if (!debug) return fn();
80
+ const start = performance.now();
81
+ try {
82
+ return fn();
83
+ } finally {
84
+ debug("%s (%sms)", label, (performance.now() - start).toFixed(1));
85
+ }
86
+ }
@@ -0,0 +1,23 @@
1
+ export interface LoaderResolveContext {
2
+ parentURL?: string;
3
+ conditions?: readonly string[];
4
+ importAttributes?: Record<string, string>;
5
+ }
6
+
7
+ export interface LoaderResolveResult {
8
+ shortCircuit?: boolean;
9
+ url: string;
10
+ format?: "module" | "commonjs" | "json" | "wasm" | null;
11
+ importAttributes?: Record<string, string>;
12
+ }
13
+
14
+ export type NextResolve = (
15
+ specifier: string,
16
+ context?: LoaderResolveContext,
17
+ ) => Promise<LoaderResolveResult>;
18
+
19
+ export function resolve(
20
+ specifier: string,
21
+ context: LoaderResolveContext,
22
+ nextResolve: NextResolve,
23
+ ): Promise<LoaderResolveResult>;
@@ -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
+ }