@rangojs/router 0.0.0-experimental.103 → 0.0.0-experimental.104
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 +4 -4
- package/dist/vite/index.js +173 -31
- package/package.json +3 -2
- package/skills/host-router/SKILL.md +45 -20
- 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/vite/discovery/discover-routers.ts +20 -22
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/state.ts +17 -0
- package/src/vite/rango.ts +16 -4
- package/src/vite/router-discovery.ts +34 -2
- package/src/vite/utils/forward-user-plugins.ts +164 -0
|
@@ -52,6 +52,10 @@ import {
|
|
|
52
52
|
import { postprocessBundle } from "./discovery/bundle-postprocess.js";
|
|
53
53
|
import { createDiscoveryGate } from "./discovery/gate-state.js";
|
|
54
54
|
import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
|
|
55
|
+
import {
|
|
56
|
+
pickForwardedRunnerConfig,
|
|
57
|
+
selectForwardableResolvePlugins,
|
|
58
|
+
} from "./utils/forward-user-plugins.js";
|
|
55
59
|
import { createRangoDebugger, timed, timedSync, NS } from "./debug.js";
|
|
56
60
|
|
|
57
61
|
const debugDiscovery = createRangoDebugger(NS.discovery);
|
|
@@ -129,14 +133,27 @@ async function createTempRscServer(
|
|
|
129
133
|
// instead of crashing Node's native loader.
|
|
130
134
|
ensureCloudflareProtocolLoaderRegistered();
|
|
131
135
|
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
136
|
+
// Mirror the user's resolution config + plugins so discovery (and the
|
|
137
|
+
// prerender/static rendering that shares this runner) resolves modules the
|
|
138
|
+
// same way the real environment does. Falls back to the legacy alias-only
|
|
139
|
+
// behavior if configResolved hasn't populated the parity slice yet.
|
|
140
|
+
const runnerConfig = state.userRunnerConfig;
|
|
141
|
+
const resolveConfig = runnerConfig?.resolve ?? {
|
|
142
|
+
alias: state.userResolveAlias,
|
|
143
|
+
};
|
|
144
|
+
const esbuildConfig = runnerConfig?.esbuild ?? {
|
|
145
|
+
jsx: "automatic",
|
|
146
|
+
jsxImportSource: "react",
|
|
147
|
+
};
|
|
132
148
|
return createViteServer({
|
|
133
149
|
root: state.projectRoot,
|
|
134
150
|
configFile: false,
|
|
135
151
|
server: { middlewareMode: true },
|
|
136
152
|
appType: "custom",
|
|
137
153
|
logLevel: "silent",
|
|
138
|
-
resolve:
|
|
139
|
-
|
|
154
|
+
resolve: resolveConfig,
|
|
155
|
+
...(runnerConfig?.define ? { define: runnerConfig.define } : {}),
|
|
156
|
+
esbuild: esbuildConfig as any,
|
|
140
157
|
...(options.cacheDir && { cacheDir: options.cacheDir }),
|
|
141
158
|
plugins: [
|
|
142
159
|
rsc({
|
|
@@ -155,6 +172,10 @@ async function createTempRscServer(
|
|
|
155
172
|
// runtime. forceBuild produces hashed IDs for production bundle consistency.
|
|
156
173
|
exposeInternalIds(options.forceBuild ? { forceBuild: true } : undefined),
|
|
157
174
|
exposeRouterId(),
|
|
175
|
+
// Forwarded user resolution plugins (e.g. vite-tsconfig-paths). Stripped
|
|
176
|
+
// to resolveId/load and placed last so framework resolution runs first;
|
|
177
|
+
// Vite re-sorts by `enforce`, so `enforce: "pre"` resolvers still lead.
|
|
178
|
+
...state.userResolvePlugins,
|
|
158
179
|
],
|
|
159
180
|
});
|
|
160
181
|
}
|
|
@@ -309,6 +330,16 @@ export function createRouterDiscoveryPlugin(
|
|
|
309
330
|
viteMode = config.mode;
|
|
310
331
|
// Capture user's resolve aliases for the temp server
|
|
311
332
|
s.userResolveAlias = config.resolve.alias;
|
|
333
|
+
// Capture the data-only resolution config (resolve.*, define, esbuild)
|
|
334
|
+
// and the user's resolution plugins (resolveId/load) so the discovery
|
|
335
|
+
// temp server resolves modules the same way the real environment does.
|
|
336
|
+
// Without this, third-party resolvers (e.g. vite-tsconfig-paths) are
|
|
337
|
+
// absent during discovery/prerender/static rendering even though they
|
|
338
|
+
// apply at request time. See utils/forward-user-plugins.ts.
|
|
339
|
+
s.userRunnerConfig = pickForwardedRunnerConfig(config);
|
|
340
|
+
s.userResolvePlugins = selectForwardableResolvePlugins(
|
|
341
|
+
config.plugins as any,
|
|
342
|
+
);
|
|
312
343
|
// Node preset: pick up auto-discovered router path from the config() hook.
|
|
313
344
|
// The auto-discover plugin runs in config() using Vite's resolved root,
|
|
314
345
|
// populating the mutable ref before configResolved fires.
|
|
@@ -1272,6 +1303,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
1272
1303
|
.join("\n");
|
|
1273
1304
|
throw new Error(
|
|
1274
1305
|
`[rsc-router] Build-time router discovery failed:\n${details}`,
|
|
1306
|
+
{ cause: err },
|
|
1275
1307
|
);
|
|
1276
1308
|
} finally {
|
|
1277
1309
|
delete (globalThis as any).__rscRouterDiscoveryActive;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery Runner Config Parity
|
|
3
|
+
*
|
|
4
|
+
* The discovery temp server (createTempRscServer) runs the user's handler
|
|
5
|
+
* graph through a throwaway Node Vite server built with `configFile: false`.
|
|
6
|
+
* Without help, that server only sees a fixed Rango-owned plugin set, so any
|
|
7
|
+
* user resolution provided by third-party plugins (e.g. vite-tsconfig-paths)
|
|
8
|
+
* is absent during discovery, prerender, and static handler rendering — even
|
|
9
|
+
* though it applies at request time.
|
|
10
|
+
*
|
|
11
|
+
* These helpers extract the resolution-relevant slice of the user's resolved
|
|
12
|
+
* config (resolve.*, define, esbuild) and forward the user's resolution
|
|
13
|
+
* plugins into the temp server so discovery resolves modules the same way the
|
|
14
|
+
* real environment does.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Plugin, ResolvedConfig, UserConfig } from "vite";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether a user plugin must NOT be forwarded into the discovery temp server.
|
|
21
|
+
*
|
|
22
|
+
* Framework-owned plugins are matched precisely -- by exact name or a
|
|
23
|
+
* namespaced prefix -- rather than by a loose substring/word prefix, so an
|
|
24
|
+
* unrelated user resolver named e.g. `rsc-paths` or `cloudflare-kv-alias` is
|
|
25
|
+
* still forwarded (it would otherwise reproduce issue #500).
|
|
26
|
+
*
|
|
27
|
+
* - `vite:*` Vite core + official plugins (incl.
|
|
28
|
+
* @vitejs/plugin-react's `vite:react-*`). The temp
|
|
29
|
+
* server already provides its own core; forwarding
|
|
30
|
+
* these would duplicate or conflict with it.
|
|
31
|
+
* - `rsc` / `rsc:*` @vitejs/plugin-rsc. createTempRscServer instantiates
|
|
32
|
+
* its own rsc() plugin; forwarding would duplicate it.
|
|
33
|
+
* Matched exactly (`rsc`) or by the `rsc:` namespace --
|
|
34
|
+
* NOT every `rsc`-prefixed name.
|
|
35
|
+
* - `@rangojs/router*` Our own plugins. The discovery plugin spawns the
|
|
36
|
+
* temp server, so forwarding would recurse infinitely.
|
|
37
|
+
* - `@cloudflare/vite-plugin*` / @cloudflare/vite-plugin (emits the scoped
|
|
38
|
+
* `vite-plugin-cloudflare*` `@cloudflare/vite-plugin` and unscoped
|
|
39
|
+
* `vite-plugin-cloudflare` / `vite-plugin-cloudflare:*`).
|
|
40
|
+
* Forwarding re-inits workerd, defeating the Node temp
|
|
41
|
+
* server. Matched specifically so a scoped user resolver
|
|
42
|
+
* like `@cloudflare/kv-alias` is still forwarded.
|
|
43
|
+
*/
|
|
44
|
+
function isDenied(name: string): boolean {
|
|
45
|
+
return (
|
|
46
|
+
name.startsWith("vite:") ||
|
|
47
|
+
name === "rsc" ||
|
|
48
|
+
name.startsWith("rsc:") ||
|
|
49
|
+
name.startsWith("@rangojs/router") ||
|
|
50
|
+
name.startsWith("@cloudflare/vite-plugin") ||
|
|
51
|
+
name.startsWith("vite-plugin-cloudflare")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A plugin participates in resolution if it exposes `resolveId` or `load`.
|
|
57
|
+
* Plugins that only transform, configure the server, or hook into the build
|
|
58
|
+
* lifecycle do not affect how bare specifiers resolve, so we skip them to keep
|
|
59
|
+
* the forwarded surface minimal.
|
|
60
|
+
*/
|
|
61
|
+
function hasResolutionHooks(p: Plugin): boolean {
|
|
62
|
+
return Boolean((p as any).resolveId || (p as any).load);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Strip a resolved plugin instance down to its resolution hooks plus the
|
|
67
|
+
* gating fields that decide whether/where it runs.
|
|
68
|
+
*
|
|
69
|
+
* We reuse the SAME instance objects captured from `config.plugins`. By the
|
|
70
|
+
* time `configResolved` fires on the discovery plugin, every plugin's own
|
|
71
|
+
* `config`/`configResolved` has already run on the main server, so any state
|
|
72
|
+
* a `resolveId` hook depends on (e.g. vite-tsconfig-paths' compiled path
|
|
73
|
+
* matcher, held in closure) is already populated. Forwarding only the
|
|
74
|
+
* resolution hooks therefore preserves correct resolution while avoiding a
|
|
75
|
+
* second `buildStart`/`configureServer`/`config` lifecycle in the temp server.
|
|
76
|
+
*
|
|
77
|
+
* `enforce` and `applyToEnvironment` are preserved so ordering and per-environment
|
|
78
|
+
* gating match the real pipeline.
|
|
79
|
+
*
|
|
80
|
+
* `apply` is intentionally dropped. Vite filters plugins by `apply` against the
|
|
81
|
+
* command during config resolution, so the `config.plugins` we read here is
|
|
82
|
+
* already command-filtered by the main server (build: `apply: "build"` +
|
|
83
|
+
* unconditional; dev: `apply: "serve"` + unconditional). The discovery temp
|
|
84
|
+
* server is always created with `createServer` (`command === "serve"`), so a
|
|
85
|
+
* forwarded `apply: "build"` plugin would be filtered straight back out -- even
|
|
86
|
+
* during a production build, where build-only resolvers are exactly what
|
|
87
|
+
* static/prerender rendering needs. Since the source list is already correct for
|
|
88
|
+
* the current command, the forwarded copy must carry no `apply` gate.
|
|
89
|
+
*/
|
|
90
|
+
function stripToResolutionHooks(p: Plugin): Plugin {
|
|
91
|
+
const stripped: Plugin = { name: p.name };
|
|
92
|
+
if ((p as any).enforce) (stripped as any).enforce = (p as any).enforce;
|
|
93
|
+
if ((p as any).applyToEnvironment)
|
|
94
|
+
(stripped as any).applyToEnvironment = (p as any).applyToEnvironment;
|
|
95
|
+
if ((p as any).resolveId) (stripped as any).resolveId = (p as any).resolveId;
|
|
96
|
+
if ((p as any).load) (stripped as any).load = (p as any).load;
|
|
97
|
+
return stripped;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Pick the user's resolution plugins from the resolved plugin list, denylist
|
|
102
|
+
* framework-owned plugins, keep only those with resolution hooks, and strip
|
|
103
|
+
* each to its resolution surface. Returns plugin objects safe to drop into the
|
|
104
|
+
* discovery temp server's `plugins` array.
|
|
105
|
+
*/
|
|
106
|
+
export function selectForwardableResolvePlugins(
|
|
107
|
+
plugins: readonly Plugin[] | undefined,
|
|
108
|
+
): Plugin[] {
|
|
109
|
+
if (!plugins) return [];
|
|
110
|
+
const forwarded: Plugin[] = [];
|
|
111
|
+
for (const p of plugins) {
|
|
112
|
+
const name = p?.name;
|
|
113
|
+
if (!name || isDenied(name)) continue;
|
|
114
|
+
if (!hasResolutionHooks(p)) continue;
|
|
115
|
+
forwarded.push(stripToResolutionHooks(p));
|
|
116
|
+
}
|
|
117
|
+
return forwarded;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The resolution-relevant slice of the user's resolved config that is plain
|
|
122
|
+
* data (no plugin re-execution): everything under `resolve` that influences
|
|
123
|
+
* how specifiers map to files, plus `define` and `esbuild` so transforms and
|
|
124
|
+
* compile-time constants match request time.
|
|
125
|
+
*/
|
|
126
|
+
export interface ForwardedRunnerConfig {
|
|
127
|
+
resolve: UserConfig["resolve"];
|
|
128
|
+
define: UserConfig["define"];
|
|
129
|
+
esbuild: UserConfig["esbuild"];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract the data-only config slice to mirror into the discovery temp server.
|
|
134
|
+
* `alias` is included here so callers no longer need to thread it separately.
|
|
135
|
+
*
|
|
136
|
+
* `esbuild` keeps the user's options but always pins the RSC-required JSX
|
|
137
|
+
* runtime, since the temp server compiles the handler graph as React server
|
|
138
|
+
* components regardless of the user's app-level JSX config.
|
|
139
|
+
*/
|
|
140
|
+
export function pickForwardedRunnerConfig(
|
|
141
|
+
config: ResolvedConfig,
|
|
142
|
+
): ForwardedRunnerConfig {
|
|
143
|
+
const r = config.resolve ?? ({} as ResolvedConfig["resolve"]);
|
|
144
|
+
const resolve: NonNullable<UserConfig["resolve"]> = {};
|
|
145
|
+
if (r.alias !== undefined) resolve.alias = r.alias as any;
|
|
146
|
+
if (r.dedupe !== undefined) resolve.dedupe = r.dedupe;
|
|
147
|
+
if (r.conditions !== undefined) resolve.conditions = r.conditions;
|
|
148
|
+
if (r.mainFields !== undefined) resolve.mainFields = r.mainFields;
|
|
149
|
+
if (r.extensions !== undefined) resolve.extensions = r.extensions;
|
|
150
|
+
if (r.preserveSymlinks !== undefined)
|
|
151
|
+
resolve.preserveSymlinks = r.preserveSymlinks;
|
|
152
|
+
|
|
153
|
+
const userEsbuild = config.esbuild;
|
|
154
|
+
const esbuild: UserConfig["esbuild"] =
|
|
155
|
+
userEsbuild && typeof userEsbuild === "object"
|
|
156
|
+
? { ...userEsbuild, jsx: "automatic", jsxImportSource: "react" }
|
|
157
|
+
: { jsx: "automatic", jsxImportSource: "react" };
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
resolve,
|
|
161
|
+
define: config.define,
|
|
162
|
+
esbuild,
|
|
163
|
+
};
|
|
164
|
+
}
|