@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dc2bd2b4
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2151 -846
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +647 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +76 -28
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +92 -182
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +53 -15
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +8 -8
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +143 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- 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/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -20,6 +20,13 @@ export interface PluginOptions {
|
|
|
20
20
|
buildEnv?: import("../plugin-types.js").BuildEnvOption;
|
|
21
21
|
/** Deployment preset (needed for buildEnv "auto" resolution). */
|
|
22
22
|
preset?: "node" | "cloudflare";
|
|
23
|
+
/**
|
|
24
|
+
* Shared context the built-in clientChunks strategy reads. Discovery populates
|
|
25
|
+
* it (registered fallback hashes + single-router name) before the client build
|
|
26
|
+
* invokes the strategy. Present only when the built-in strategy is active
|
|
27
|
+
* (`clientChunks: true`/default); undefined for `false` or a custom function.
|
|
28
|
+
*/
|
|
29
|
+
clientChunkCtx?: import("../utils/client-chunks.js").ClientChunkContext;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
export interface PrecomputedEntry {
|
|
@@ -45,6 +52,21 @@ export interface DiscoveryState {
|
|
|
45
52
|
projectRoot: string;
|
|
46
53
|
isBuildMode: boolean;
|
|
47
54
|
userResolveAlias: any;
|
|
55
|
+
/**
|
|
56
|
+
* Data-only slice of the user's resolved config (resolve.* incl. native
|
|
57
|
+
* tsconfigPaths, define, oxc) mirrored into the discovery temp server so it
|
|
58
|
+
* resolves and transforms modules the same way the real environment does.
|
|
59
|
+
* See `utils/forward-user-plugins.ts`.
|
|
60
|
+
*/
|
|
61
|
+
userRunnerConfig:
|
|
62
|
+
| import("../utils/forward-user-plugins.js").ForwardedRunnerConfig
|
|
63
|
+
| undefined;
|
|
64
|
+
/**
|
|
65
|
+
* User resolution plugins (resolveId/load), stripped to their resolution
|
|
66
|
+
* surface, forwarded into the discovery temp server. Lets third-party
|
|
67
|
+
* resolvers such as vite-tsconfig-paths participate in discovery.
|
|
68
|
+
*/
|
|
69
|
+
userResolvePlugins: import("vite").Plugin[];
|
|
48
70
|
scanFilter: ScanFilter | undefined;
|
|
49
71
|
cachedRouterFiles: string[] | undefined;
|
|
50
72
|
opts: PluginOptions | undefined;
|
|
@@ -76,6 +98,14 @@ export interface DiscoveryState {
|
|
|
76
98
|
resolvedBuildEnv?: Record<string, unknown>;
|
|
77
99
|
/** Cleanup function for build-time env resources (e.g., miniflare). */
|
|
78
100
|
buildEnvDispose?: (() => Promise<void> | void) | null;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Set when the most recent HMR re-discovery threw. Cleared on the next
|
|
104
|
+
* successful discovery. Surfaced via debug logs so we can detect "manifest
|
|
105
|
+
* frozen at last-good after error → user fix in non-route file → no
|
|
106
|
+
* rediscovery trigger" scenarios.
|
|
107
|
+
*/
|
|
108
|
+
lastDiscoveryError?: { message: string; at: number } | null;
|
|
79
109
|
}
|
|
80
110
|
|
|
81
111
|
export function createDiscoveryState(
|
|
@@ -87,6 +117,8 @@ export function createDiscoveryState(
|
|
|
87
117
|
projectRoot: "",
|
|
88
118
|
isBuildMode: false,
|
|
89
119
|
userResolveAlias: undefined,
|
|
120
|
+
userRunnerConfig: undefined,
|
|
121
|
+
userResolvePlugins: [],
|
|
90
122
|
scanFilter: undefined,
|
|
91
123
|
cachedRouterFiles: undefined,
|
|
92
124
|
opts,
|
|
@@ -113,5 +145,6 @@ export function createDiscoveryState(
|
|
|
113
145
|
devServer: null,
|
|
114
146
|
selfWrittenGenFiles: new Map(),
|
|
115
147
|
SELF_WRITE_WINDOW_MS: 5_000,
|
|
148
|
+
lastDiscoveryError: null,
|
|
116
149
|
};
|
|
117
150
|
}
|
|
@@ -58,7 +58,7 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const lines = [
|
|
61
|
-
`import { setCachedManifest,
|
|
61
|
+
`import { setCachedManifest, setRouterManifest, registerRouterManifestLoader, clearAllRouterData } from "@rangojs/router/server";`,
|
|
62
62
|
...genFileImports,
|
|
63
63
|
// Clear stale per-router cached data (manifest, trie, precomputed entries)
|
|
64
64
|
// before re-populating. In Cloudflare dev mode, program reloads re-evaluate
|
|
@@ -101,28 +101,18 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
lines.push(
|
|
117
|
-
`setPrecomputedEntries(${jsonParseExpression(state.mergedPrecomputedEntries)});`,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
if (state.mergedRouteTrie) {
|
|
121
|
-
lines.push(
|
|
122
|
-
`setRouteTrie(${jsonParseExpression(state.mergedRouteTrie)});`,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
104
|
+
// Per-router trie and precomputedEntries are NOT inlined eagerly.
|
|
105
|
+
// They live in the per-router lazy chunks (generatePerRouterModule) and
|
|
106
|
+
// are loaded via ensureRouterManifest(routerId), which is awaited before
|
|
107
|
+
// every request in router.fetch() and before findMatch is reached.
|
|
108
|
+
// Inlining the merged versions here would duplicate the per-router data
|
|
109
|
+
// (the merged trie/precomputedEntries equal the per-router data for
|
|
110
|
+
// single-router apps; for multi-router, the merged trie is dead code
|
|
111
|
+
// because find-match.ts only consumes per-router tries).
|
|
112
|
+
//
|
|
113
|
+
// In dev mode, the handler also falls back to Phase 2 regex matching
|
|
114
|
+
// against live router.urlpatterns, which is always correct after a
|
|
115
|
+
// program reload.
|
|
126
116
|
|
|
127
117
|
// Register lazy loaders for per-router manifest modules.
|
|
128
118
|
// Each import() uses a static string literal so Rollup creates separate chunks.
|
package/src/vite/index.ts
CHANGED
package/src/vite/plugin-types.ts
CHANGED
|
@@ -47,6 +47,64 @@ export type BuildEnvOption =
|
|
|
47
47
|
| Record<string, unknown>
|
|
48
48
|
| BuildEnvFactory;
|
|
49
49
|
|
|
50
|
+
// -- Client chunking --------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Metadata for one client ("use client") module, passed to a {@link ClientChunks}
|
|
54
|
+
* function. Mirrors the shape `@vitejs/plugin-rsc` passes to its own
|
|
55
|
+
* `clientChunks` option.
|
|
56
|
+
*/
|
|
57
|
+
export interface ClientChunkMeta {
|
|
58
|
+
/** Absolute module id of the "use client" file. */
|
|
59
|
+
id: string;
|
|
60
|
+
/** Normalized (posix) module id — convenient for path-based matching. */
|
|
61
|
+
normalizedId: string;
|
|
62
|
+
/**
|
|
63
|
+
* The RSC/server chunk that statically imports this client reference. This is
|
|
64
|
+
* the key used for the default grouping when no override is supplied: a single
|
|
65
|
+
* router that statically imports every route yields ONE `serverChunk`, hence
|
|
66
|
+
* one client chunk for all routes.
|
|
67
|
+
*/
|
|
68
|
+
serverChunk: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Controls how client ("use client") components are grouped into browser
|
|
73
|
+
* chunks, i.e. per-route / per-feature code splitting of the client bundle.
|
|
74
|
+
*
|
|
75
|
+
* Without splitting, a single router ships ONE client chunk containing every
|
|
76
|
+
* route's client components (and their CSS) — navigating to one route downloads
|
|
77
|
+
* every other route's client code. (Host sub-apps loaded via a dynamic `import()`
|
|
78
|
+
* are the exception: each forms its own chunk.) This option controls how that
|
|
79
|
+
* monolith is split.
|
|
80
|
+
*
|
|
81
|
+
* Behavior branches:
|
|
82
|
+
* - `true` / omitted (**default**, pre-1.0): Rango's built-in **directory
|
|
83
|
+
* strategy**. It splits app `"use client"` modules by **route id** — the segment
|
|
84
|
+
* after a route-root directory (`routes`, `app`, `pages`, `features`, `handlers`,
|
|
85
|
+
* …) — so `routes/dashboard/**` becomes `app-dashboard` at any nesting depth.
|
|
86
|
+
* Where it finds NO route structure (a flat `src/components/`, or host sub-apps
|
|
87
|
+
* already split by a dynamic `import()`), it inherits the default grouping
|
|
88
|
+
* unchanged — so the shared `src/components` chunk stays shared and host apps do
|
|
89
|
+
* not leak across each other. Shared runtime (React, the router, `node_modules`)
|
|
90
|
+
* is never split.
|
|
91
|
+
* - `false`: opt out — inherit `@vitejs/plugin-rsc`'s default grouping everywhere
|
|
92
|
+
* (one chunk per router / per host sub-app).
|
|
93
|
+
* - function: full override. Return a chunk group name, or `undefined` to fall
|
|
94
|
+
* back to the default grouping for that one module. Forwarded directly to
|
|
95
|
+
* `@vitejs/plugin-rsc`'s `clientChunks`.
|
|
96
|
+
*
|
|
97
|
+
* Every module maps to exactly one group, so there is no byte duplication: a
|
|
98
|
+
* component used by two routes lives in one group and is fetched whenever it
|
|
99
|
+
* renders. Put genuinely shared client components OUTSIDE route directories so
|
|
100
|
+
* they land in the shared group rather than one route's chunk.
|
|
101
|
+
*
|
|
102
|
+
* @default true
|
|
103
|
+
*/
|
|
104
|
+
export type ClientChunks =
|
|
105
|
+
| boolean
|
|
106
|
+
| ((meta: ClientChunkMeta) => string | undefined);
|
|
107
|
+
|
|
50
108
|
// -- Plugin options ---------------------------------------------------------
|
|
51
109
|
|
|
52
110
|
/**
|
|
@@ -59,6 +117,15 @@ interface RangoBaseOptions {
|
|
|
59
117
|
*/
|
|
60
118
|
banner?: boolean;
|
|
61
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Group client ("use client") components into browser chunks for per-route /
|
|
122
|
+
* per-feature code splitting. On by default (pre-1.0); pass `false` to opt out.
|
|
123
|
+
* See {@link ClientChunks}.
|
|
124
|
+
*
|
|
125
|
+
* @default true
|
|
126
|
+
*/
|
|
127
|
+
clientChunks?: ClientChunks;
|
|
128
|
+
|
|
62
129
|
/**
|
|
63
130
|
* Environment bindings available to Prerender and Static handlers at build
|
|
64
131
|
* time via `ctx.env`. Applies to both production build and dev on-demand
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
3
|
+
|
|
4
|
+
const debug = createRangoDebugger(NS.transform);
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Transform CJS vendor files from @vitejs/plugin-rsc to ESM for browser compatibility.
|
|
@@ -9,18 +12,16 @@ export function createCjsToEsmPlugin(): Plugin {
|
|
|
9
12
|
name: "@rangojs/router:cjs-to-esm",
|
|
10
13
|
enforce: "pre",
|
|
11
14
|
transform(code, id) {
|
|
12
|
-
const cleanId = id.split("?")[0];
|
|
15
|
+
const cleanId = id.split("?")[0].replaceAll("\\", "/");
|
|
13
16
|
|
|
14
17
|
// Transform the client.browser.js entry point to re-export from CJS
|
|
15
|
-
if (
|
|
16
|
-
cleanId.includes("vendor/react-server-dom/client.browser.js") ||
|
|
17
|
-
cleanId.includes("vendor\\react-server-dom\\client.browser.js")
|
|
18
|
-
) {
|
|
18
|
+
if (cleanId.includes("vendor/react-server-dom/client.browser.js")) {
|
|
19
19
|
const isProd = process.env.NODE_ENV === "production";
|
|
20
20
|
const cjsFile = isProd
|
|
21
21
|
? "./cjs/react-server-dom-webpack-client.browser.production.js"
|
|
22
22
|
: "./cjs/react-server-dom-webpack-client.browser.development.js";
|
|
23
23
|
|
|
24
|
+
debug?.("cjs-to-esm entry redirect %s", id);
|
|
24
25
|
return {
|
|
25
26
|
code: `export * from "${cjsFile}";`,
|
|
26
27
|
map: null,
|
|
@@ -29,8 +30,7 @@ export function createCjsToEsmPlugin(): Plugin {
|
|
|
29
30
|
|
|
30
31
|
// Transform the actual CJS files to ESM
|
|
31
32
|
if (
|
|
32
|
-
|
|
33
|
-
cleanId.includes("vendor\\react-server-dom\\cjs\\")) &&
|
|
33
|
+
cleanId.includes("vendor/react-server-dom/cjs/") &&
|
|
34
34
|
cleanId.includes("client.browser")
|
|
35
35
|
) {
|
|
36
36
|
let transformed = code;
|
|
@@ -81,6 +81,7 @@ export function createCjsToEsmPlugin(): Plugin {
|
|
|
81
81
|
// Reconstruct with license at the top
|
|
82
82
|
transformed = license + "\n" + transformed;
|
|
83
83
|
|
|
84
|
+
debug?.("cjs-to-esm body rewrite %s", id);
|
|
84
85
|
return {
|
|
85
86
|
code: transformed,
|
|
86
87
|
map: null,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Plugin, ResolvedConfig } from "vite";
|
|
2
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
3
|
+
|
|
4
|
+
const debug = createRangoDebugger(NS.transform);
|
|
2
5
|
|
|
3
6
|
const CLIENT_IN_SERVER_PROXY_PREFIX =
|
|
4
7
|
"virtual:vite-rsc/client-in-server-package-proxy/";
|
|
@@ -62,6 +65,7 @@ export function extractPackageName(absolutePath: string): string | null {
|
|
|
62
65
|
*/
|
|
63
66
|
export function clientRefDedup(): Plugin {
|
|
64
67
|
let clientExclude: string[] = [];
|
|
68
|
+
const dedupedPackages = new Set<string>();
|
|
65
69
|
|
|
66
70
|
return {
|
|
67
71
|
name: "@rangojs/router:client-ref-dedup",
|
|
@@ -76,6 +80,16 @@ export function clientRefDedup(): Plugin {
|
|
|
76
80
|
clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
|
|
77
81
|
},
|
|
78
82
|
|
|
83
|
+
buildEnd() {
|
|
84
|
+
if (debug && dedupedPackages.size > 0) {
|
|
85
|
+
debug(
|
|
86
|
+
"client-ref-dedup: redirected %d package(s) (%s)",
|
|
87
|
+
dedupedPackages.size,
|
|
88
|
+
[...dedupedPackages].join(","),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
79
93
|
resolveId(source, importer, options) {
|
|
80
94
|
// Only intercept in the client environment
|
|
81
95
|
if (this.environment?.name !== "client") return;
|
|
@@ -95,6 +109,8 @@ export function clientRefDedup(): Plugin {
|
|
|
95
109
|
// Don't redirect packages that are excluded from optimization
|
|
96
110
|
if (clientExclude.includes(packageName)) return;
|
|
97
111
|
|
|
112
|
+
if (debug) dedupedPackages.add(packageName);
|
|
113
|
+
|
|
98
114
|
// Return a virtual module that re-exports via bare specifier
|
|
99
115
|
return `\0rango:dedup/${packageName}`;
|
|
100
116
|
},
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
import { relative } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { createRangoDebugger, createCounter, NS } from "../debug.js";
|
|
5
|
+
|
|
6
|
+
const debug = createRangoDebugger(NS.transform);
|
|
4
7
|
|
|
5
8
|
// Dev-mode client-reference key prefixes emitted by @vitejs/plugin-rsc
|
|
6
9
|
const CLIENT_PKG_PROXY_PREFIX =
|
|
@@ -19,6 +22,17 @@ const FS_PREFIX = "/@fs/";
|
|
|
19
22
|
* Returns the input unchanged if it doesn't match a known dev-mode pattern
|
|
20
23
|
* (e.g., already a production hash).
|
|
21
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* The production client-reference key hash: `sha256(relativeId).slice(0,12)`,
|
|
27
|
+
* matching @vitejs/plugin-rsc's `hashString`. Exported so the client-chunks
|
|
28
|
+
* strategy can hash a `clientChunks` callback's `meta.normalizedId` (already the
|
|
29
|
+
* project-root-relative id) and compare it against fallback hashes collected
|
|
30
|
+
* during discovery.
|
|
31
|
+
*/
|
|
32
|
+
export function hashRefKey(relativeId: string): string {
|
|
33
|
+
return createHash("sha256").update(relativeId).digest("hex").slice(0, 12);
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
export function computeProductionHash(
|
|
23
37
|
projectRoot: string,
|
|
24
38
|
refKey: string,
|
|
@@ -46,7 +60,7 @@ export function computeProductionHash(
|
|
|
46
60
|
return refKey;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
return
|
|
63
|
+
return hashRefKey(toHash);
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
// Regex to match registerClientReference() calls as emitted by @vitejs/plugin-rsc.
|
|
@@ -89,6 +103,7 @@ export function transformClientRefs(
|
|
|
89
103
|
* regex replacement of Flight payloads.
|
|
90
104
|
*/
|
|
91
105
|
export function hashClientRefs(projectRoot: string): Plugin {
|
|
106
|
+
const counter = createCounter(debug, "hash-client-refs");
|
|
92
107
|
return {
|
|
93
108
|
name: "@rangojs/router:hash-client-refs",
|
|
94
109
|
// Run after the RSC plugin's transform (default enforce is normal)
|
|
@@ -96,10 +111,18 @@ export function hashClientRefs(projectRoot: string): Plugin {
|
|
|
96
111
|
applyToEnvironment(env) {
|
|
97
112
|
return env.name === "rsc";
|
|
98
113
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
buildEnd() {
|
|
115
|
+
counter?.flush();
|
|
116
|
+
},
|
|
117
|
+
transform(code, id) {
|
|
118
|
+
const start = counter ? performance.now() : 0;
|
|
119
|
+
try {
|
|
120
|
+
const result = transformClientRefs(code, projectRoot);
|
|
121
|
+
if (result === null) return;
|
|
122
|
+
return { code: result, map: null };
|
|
123
|
+
} finally {
|
|
124
|
+
counter?.record(id, performance.now() - start);
|
|
125
|
+
}
|
|
103
126
|
},
|
|
104
127
|
};
|
|
105
128
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
const VIRTUAL_PREFIX = "virtual:rango-cloudflare-stub-";
|
|
4
|
+
const NULL_PREFIX = "\0" + VIRTUAL_PREFIX;
|
|
5
|
+
const CF_PREFIX = "cloudflare:";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `globalThis` key the `cloudflare:workers` stub reads to populate its
|
|
9
|
+
* `env` export. Router discovery sets this to the resolved `buildEnv`
|
|
10
|
+
* proxy (from `wrangler.getPlatformProxy()` when `buildEnv: "auto"` is
|
|
11
|
+
* configured, or a user-supplied object otherwise) before importing the
|
|
12
|
+
* worker entry, and clears it after discovery disposes the proxy. When
|
|
13
|
+
* unset, the stub's `env` falls back to `{}`.
|
|
14
|
+
*
|
|
15
|
+
* Using `globalThis` is the only cross-module bridge that works here:
|
|
16
|
+
* the stub's `load` hook returns source text, not a live closure, but
|
|
17
|
+
* the stub module is evaluated in the same Node process as the
|
|
18
|
+
* discovery plugin — so reading a global at module-evaluation time
|
|
19
|
+
* reaches whatever the plugin assigned there. A symbol key would be
|
|
20
|
+
* cleaner in-process but awkward to name from the stub source.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export const BUILD_ENV_GLOBAL_KEY = "__rango_build_env__";
|
|
25
|
+
|
|
26
|
+
const SOURCE_EXT_RE = /\.[mc]?[jt]sx?$/;
|
|
27
|
+
|
|
28
|
+
const IMPORT_NODE_TYPES = new Set([
|
|
29
|
+
"ImportDeclaration",
|
|
30
|
+
"ImportExpression",
|
|
31
|
+
"ExportNamedDeclaration",
|
|
32
|
+
"ExportAllDeclaration",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// Keep in sync with `STUBS` in cloudflare-protocol-loader-hook.mjs —
|
|
36
|
+
// both paths (Vite transform and Node loader) need to hand out the same
|
|
37
|
+
// classes. Unknown `cloudflare:*` modules fall back to an empty default
|
|
38
|
+
// export so third-party packages (e.g. the Cloudflare Agents SDK) can
|
|
39
|
+
// pull them into the graph without crashing discovery. Discovery only
|
|
40
|
+
// evaluates module top-level code — no handlers run — so missing named
|
|
41
|
+
// exports only fail if something does `class X extends Missing {}` at
|
|
42
|
+
// module scope, which is rare outside the already-stubbed classes.
|
|
43
|
+
const STUBS: Record<string, string> = {
|
|
44
|
+
"cloudflare:workers": `
|
|
45
|
+
export class DurableObject { constructor(_ctx, _env) {} }
|
|
46
|
+
export class WorkerEntrypoint { constructor(_ctx, _env) {} }
|
|
47
|
+
export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
|
|
48
|
+
export class RpcTarget {}
|
|
49
|
+
export const env = globalThis[${JSON.stringify(BUILD_ENV_GLOBAL_KEY)}] ?? {};
|
|
50
|
+
export default {};
|
|
51
|
+
`,
|
|
52
|
+
"cloudflare:email": `
|
|
53
|
+
export class EmailMessage { constructor(_from, _to, _raw) {} }
|
|
54
|
+
export default {};
|
|
55
|
+
`,
|
|
56
|
+
"cloudflare:sockets": `
|
|
57
|
+
export function connect() { return {}; }
|
|
58
|
+
export default {};
|
|
59
|
+
`,
|
|
60
|
+
"cloudflare:workflows": `
|
|
61
|
+
export class NonRetryableError extends Error {
|
|
62
|
+
constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
|
|
63
|
+
}
|
|
64
|
+
export default {};
|
|
65
|
+
`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Policy: unknown `cloudflare:*` specifiers resolve permissively (empty
|
|
69
|
+
// default export) rather than throwing. We prioritize dependency-graph
|
|
70
|
+
// resilience over strict validation of user imports because third-party
|
|
71
|
+
// packages can pull `cloudflare:*` modules we haven't curated, and
|
|
72
|
+
// discovery should not fail just because those modules appear in the graph.
|
|
73
|
+
// Tradeoff: unsupported user-authored `cloudflare:*` imports may fail later
|
|
74
|
+
// with a generic JS/module error instead of a tailored rango-branded hint.
|
|
75
|
+
// The test below pins this behavior so dependency compatibility is not
|
|
76
|
+
// regressed accidentally.
|
|
77
|
+
const FALLBACK_STUB = `export default {};\n`;
|
|
78
|
+
|
|
79
|
+
interface AstNode {
|
|
80
|
+
type: string;
|
|
81
|
+
start?: number;
|
|
82
|
+
end?: number;
|
|
83
|
+
source?: AstNode | null;
|
|
84
|
+
value?: unknown;
|
|
85
|
+
[key: string]: unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Stubs `cloudflare:*` imports for the discovery-time Node Vite server.
|
|
90
|
+
*
|
|
91
|
+
* Discovery only evaluates user module top-level code — it never invokes
|
|
92
|
+
* DurableObject / WorkerEntrypoint / Workflow handlers — so empty base
|
|
93
|
+
* classes are enough for `class X extends DurableObject {}` declarations
|
|
94
|
+
* to load in Node, where `cloudflare:*` is otherwise unresolvable.
|
|
95
|
+
*
|
|
96
|
+
* Interception point: a transform hook parses source with Rollup's
|
|
97
|
+
* plugin-context parser (`this.parse`) and rewrites only real import
|
|
98
|
+
* specifier spans (`import ... from "cloudflare:xxx"`,
|
|
99
|
+
* `import("cloudflare:xxx")`, `export ... from "cloudflare:xxx"`) to a
|
|
100
|
+
* plain virtual module name (`virtual:rango-cloudflare-stub-xxx`).
|
|
101
|
+
* This must be done in transform because Vite's module runner routes
|
|
102
|
+
* URL-scheme specifiers straight to Node's native ESM loader without
|
|
103
|
+
* consulting plugin `resolveId` hooks. Using the AST (instead of a
|
|
104
|
+
* text regex or a permissive lexer) guarantees that strings,
|
|
105
|
+
* comments, and template literals that merely contain import-like
|
|
106
|
+
* text are never mutated — the walker only looks at the four import
|
|
107
|
+
* node types.
|
|
108
|
+
*
|
|
109
|
+
* The transform runs on user source AND on compiled node_modules
|
|
110
|
+
* output: real-world CF packages (e.g. the Cloudflare Agents SDK)
|
|
111
|
+
* ship compiled JS that contains `import ... from "cloudflare:email"`
|
|
112
|
+
* and similar, so excluding node_modules would leave those imports
|
|
113
|
+
* unrewritten. Cost is small because the early exit (`code.includes`)
|
|
114
|
+
* skips files with no cloudflare: mention.
|
|
115
|
+
*
|
|
116
|
+
* The plugin intentionally runs at Vite's default ordering (no
|
|
117
|
+
* `enforce: "pre"`) so TS/JSX has already been compiled to plain JS
|
|
118
|
+
* by the time `this.parse` runs — acorn doesn't understand
|
|
119
|
+
* non-standard syntax.
|
|
120
|
+
*
|
|
121
|
+
* `cloudflare:workers`, `cloudflare:email`, `cloudflare:sockets`, and
|
|
122
|
+
* `cloudflare:workflows` each get curated stubs with the well-known
|
|
123
|
+
* symbols that appear in top-level `extends` positions. Any other
|
|
124
|
+
* `cloudflare:*` specifier falls back to an empty default export —
|
|
125
|
+
* discovery never executes the handlers, so an empty module is safe
|
|
126
|
+
* for anything the graph pulls in transitively.
|
|
127
|
+
*
|
|
128
|
+
* Only registered in the discovery temp server, not the user's runtime
|
|
129
|
+
* config.
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
export function createCloudflareProtocolStubPlugin(): Plugin {
|
|
133
|
+
return {
|
|
134
|
+
name: "@rangojs/router:cloudflare-protocol-stub",
|
|
135
|
+
transform(code, id) {
|
|
136
|
+
const cleanId = id.split("?")[0] ?? id;
|
|
137
|
+
if (!SOURCE_EXT_RE.test(cleanId)) return null;
|
|
138
|
+
if (!code.includes(CF_PREFIX)) return null;
|
|
139
|
+
|
|
140
|
+
let ast: AstNode;
|
|
141
|
+
try {
|
|
142
|
+
ast = this.parse(code, { lang: "tsx" }) as unknown as AstNode;
|
|
143
|
+
} catch {
|
|
144
|
+
// Malformed source — let a downstream plugin surface the parse error.
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const hits: Array<{ start: number; end: number; value: string }> = [];
|
|
149
|
+
walk(ast, (node) => {
|
|
150
|
+
if (!IMPORT_NODE_TYPES.has(node.type)) return;
|
|
151
|
+
const source = node.source;
|
|
152
|
+
if (!source || source.type !== "Literal") return;
|
|
153
|
+
if (typeof source.value !== "string") return;
|
|
154
|
+
if (!source.value.startsWith(CF_PREFIX)) return;
|
|
155
|
+
if (typeof source.start !== "number" || typeof source.end !== "number")
|
|
156
|
+
return;
|
|
157
|
+
hits.push({
|
|
158
|
+
start: source.start,
|
|
159
|
+
end: source.end,
|
|
160
|
+
value: source.value,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (hits.length === 0) return null;
|
|
165
|
+
|
|
166
|
+
// Rewrite from last to first so earlier offsets stay valid. `start`/
|
|
167
|
+
// `end` span the full literal including quotes, so we re-emit the
|
|
168
|
+
// same quote character around the new specifier.
|
|
169
|
+
hits.sort((a, b) => b.start - a.start);
|
|
170
|
+
let out = code;
|
|
171
|
+
for (const hit of hits) {
|
|
172
|
+
const submodule = hit.value.slice(CF_PREFIX.length);
|
|
173
|
+
const quote = code[hit.start] === "'" ? "'" : '"';
|
|
174
|
+
out =
|
|
175
|
+
out.slice(0, hit.start) +
|
|
176
|
+
quote +
|
|
177
|
+
VIRTUAL_PREFIX +
|
|
178
|
+
submodule +
|
|
179
|
+
quote +
|
|
180
|
+
out.slice(hit.end);
|
|
181
|
+
}
|
|
182
|
+
return { code: out, map: null };
|
|
183
|
+
},
|
|
184
|
+
resolveId(id) {
|
|
185
|
+
if (id.startsWith(VIRTUAL_PREFIX)) {
|
|
186
|
+
return "\0" + id;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
},
|
|
190
|
+
load(id) {
|
|
191
|
+
if (!id.startsWith(NULL_PREFIX)) return null;
|
|
192
|
+
const submodule = id.slice(NULL_PREFIX.length);
|
|
193
|
+
const specifier = CF_PREFIX + submodule;
|
|
194
|
+
return STUBS[specifier] ?? FALLBACK_STUB;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function walk(node: unknown, visit: (n: AstNode) => void): void {
|
|
200
|
+
if (!node || typeof node !== "object") return;
|
|
201
|
+
if (Array.isArray(node)) {
|
|
202
|
+
for (const child of node) walk(child, visit);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const n = node as AstNode;
|
|
206
|
+
if (typeof n.type !== "string") return;
|
|
207
|
+
visit(n);
|
|
208
|
+
for (const key in n) {
|
|
209
|
+
if (key === "loc" || key === "start" || key === "end" || key === "range") {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
walk(n[key], visit);
|
|
213
|
+
}
|
|
214
|
+
}
|