@rangojs/router 0.0.0-experimental.61 → 0.0.0-experimental.63

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 (41) hide show
  1. package/README.md +61 -8
  2. package/dist/bin/rango.js +2 -1
  3. package/dist/vite/index.js +144 -62
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/package.json +14 -15
  6. package/skills/prerender/SKILL.md +110 -68
  7. package/src/__internal.ts +1 -1
  8. package/src/build/generate-manifest.ts +3 -6
  9. package/src/build/route-types/scan-filter.ts +8 -1
  10. package/src/index.rsc.ts +3 -1
  11. package/src/index.ts +8 -0
  12. package/src/prerender/store.ts +5 -4
  13. package/src/prerender.ts +138 -77
  14. package/src/reverse.ts +2 -0
  15. package/src/route-definition/dsl-helpers.ts +37 -18
  16. package/src/route-definition/index.ts +3 -0
  17. package/src/route-definition/resolve-handler-use.ts +149 -0
  18. package/src/route-types.ts +11 -0
  19. package/src/router/handler-context.ts +22 -5
  20. package/src/router/match-api.ts +2 -8
  21. package/src/router/match-middleware/cache-lookup.ts +2 -6
  22. package/src/router/prerender-match.ts +104 -8
  23. package/src/router/router-interfaces.ts +4 -0
  24. package/src/router/segment-resolution/fresh.ts +7 -2
  25. package/src/router/segment-resolution/revalidation.ts +10 -5
  26. package/src/router.ts +9 -1
  27. package/src/server/context.ts +5 -1
  28. package/src/static-handler.ts +18 -6
  29. package/src/types/handler-context.ts +12 -2
  30. package/src/types/route-entry.ts +1 -1
  31. package/src/urls/path-helper-types.ts +5 -1
  32. package/src/urls/path-helper.ts +47 -12
  33. package/src/urls/response-types.ts +16 -6
  34. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  35. package/src/vite/discovery/prerender-collection.ts +14 -1
  36. package/src/vite/discovery/state.ts +13 -4
  37. package/src/vite/index.ts +4 -0
  38. package/src/vite/plugin-types.ts +60 -5
  39. package/src/vite/rango.ts +2 -1
  40. package/src/vite/router-discovery.ts +153 -34
  41. package/src/vite/utils/prerender-utils.ts +2 -0
@@ -10,6 +10,8 @@ import type { Plugin } from "vite";
10
10
  import { createServer as createViteServer } from "vite";
11
11
  import { resolve } from "node:path";
12
12
  import { readFileSync } from "node:fs";
13
+ import { createRequire } from "node:module";
14
+ import { pathToFileURL } from "node:url";
13
15
  import {
14
16
  formatNestedRouterConflictError,
15
17
  findNestedRouterConflict,
@@ -94,6 +96,105 @@ async function createTempRscServer(
94
96
  });
95
97
  }
96
98
 
99
+ // ============================================================================
100
+ // Build-Time Env Resolution
101
+ // ============================================================================
102
+
103
+ import type {
104
+ BuildEnvOption,
105
+ BuildEnvFactoryContext,
106
+ BuildEnvResult,
107
+ } from "./plugin-types.js";
108
+
109
+ /**
110
+ * Resolve the buildEnv option into a concrete { env, dispose? } result.
111
+ * Handles all four input shapes: false, "auto", factory, plain object.
112
+ */
113
+ async function resolveBuildEnv(
114
+ option: BuildEnvOption | undefined,
115
+ factoryCtx: BuildEnvFactoryContext,
116
+ ): Promise<BuildEnvResult | null> {
117
+ if (!option) return null;
118
+
119
+ if (option === "auto") {
120
+ if (factoryCtx.preset !== "cloudflare") {
121
+ throw new Error(
122
+ '[rsc-router] buildEnv: "auto" is only supported with preset: "cloudflare". ' +
123
+ "Use a factory function or plain object for other presets.",
124
+ );
125
+ }
126
+ try {
127
+ // Resolve wrangler from the user's project root (not the router package)
128
+ const userRequire = createRequire(
129
+ resolve(factoryCtx.root, "package.json"),
130
+ );
131
+ const wranglerPath = userRequire.resolve("wrangler");
132
+ const { getPlatformProxy } = (await import(
133
+ pathToFileURL(wranglerPath).href
134
+ )) as {
135
+ getPlatformProxy: (opts?: any) => Promise<any>;
136
+ };
137
+ const proxy = await getPlatformProxy();
138
+ return {
139
+ env: proxy.env as Record<string, unknown>,
140
+ dispose: proxy.dispose,
141
+ };
142
+ } catch (err: any) {
143
+ throw new Error(
144
+ '[rsc-router] buildEnv: "auto" requires wrangler to be installed.\n' +
145
+ `Install it with: pnpm add -D wrangler\n${err.message}`,
146
+ );
147
+ }
148
+ }
149
+
150
+ if (typeof option === "function") {
151
+ return await option(factoryCtx);
152
+ }
153
+
154
+ // Plain object
155
+ return { env: option };
156
+ }
157
+
158
+ /**
159
+ * Acquire build-time env bindings and store on discovery state.
160
+ * Returns true if env was acquired, false if buildEnv is disabled.
161
+ */
162
+ async function acquireBuildEnv(
163
+ s: DiscoveryState,
164
+ command: "serve" | "build",
165
+ mode: string,
166
+ ): Promise<boolean> {
167
+ const option = s.opts?.buildEnv;
168
+ if (!option) return false;
169
+
170
+ const result = await resolveBuildEnv(option, {
171
+ root: s.projectRoot,
172
+ mode,
173
+ command,
174
+ preset: s.opts?.preset ?? "node",
175
+ });
176
+ if (!result) return false;
177
+
178
+ s.resolvedBuildEnv = result.env;
179
+ s.buildEnvDispose = result.dispose ?? null;
180
+ return true;
181
+ }
182
+
183
+ /**
184
+ * Release build-time env resources and clear state.
185
+ */
186
+ async function releaseBuildEnv(s: DiscoveryState): Promise<void> {
187
+ if (s.buildEnvDispose) {
188
+ try {
189
+ await s.buildEnvDispose();
190
+ } catch (err: any) {
191
+ console.warn(`[rsc-router] buildEnv dispose failed: ${err.message}`);
192
+ }
193
+ s.buildEnvDispose = null;
194
+ }
195
+ s.resolvedBuildEnv = undefined;
196
+ }
197
+
97
198
  /**
98
199
  * Plugin that discovers router instances at dev/build time via the RSC environment.
99
200
  *
@@ -111,6 +212,8 @@ export function createRouterDiscoveryPlugin(
111
212
  opts?: PluginOptions,
112
213
  ): Plugin {
113
214
  const s = createDiscoveryState(entryPath, opts);
215
+ let viteCommand: "serve" | "build" = "build";
216
+ let viteMode = "production";
114
217
 
115
218
  return {
116
219
  name: "@rangojs/router:discovery",
@@ -121,32 +224,20 @@ export function createRouterDiscoveryPlugin(
121
224
  __RANGO_DEBUG__: JSON.stringify(!!process.env.INTERNAL_RANGO_DEBUG),
122
225
  },
123
226
  };
124
- if (opts?.enableBuildPrerender) {
125
- config.environments = {
126
- rsc: {
127
- build: {
128
- rollupOptions: {
129
- output: {
130
- manualChunks(id: string) {
131
- if (s.resolvedPrerenderModules?.has(id)) {
132
- return "__prerender-handlers";
133
- }
134
- if (s.resolvedStaticModules?.has(id)) {
135
- return "__static-handlers";
136
- }
137
- },
138
- },
139
- },
140
- },
141
- },
142
- };
143
- }
227
+ // Prerender/static handler modules are bundled naturally with the
228
+ // rest of the RSC entry. A previous design forced them into dedicated
229
+ // __prerender-handlers / __static-handlers chunks via manualChunks,
230
+ // but Rollup hoisted all shared dependencies into those chunks,
231
+ // inflating them to ~1 MB with active runtime code. Handler code is
232
+ // evicted in closeBundle regardless of which chunk it lands in.
144
233
  return config;
145
234
  },
146
235
 
147
236
  configResolved(config) {
148
237
  s.projectRoot = config.root;
149
238
  s.isBuildMode = config.command === "build";
239
+ viteCommand = config.command as "serve" | "build";
240
+ viteMode = config.mode;
150
241
  // Capture user's resolve aliases for the temp server
151
242
  s.userResolveAlias = config.resolve.alias;
152
243
  // Node preset: pick up auto-discovered router path from the config() hook.
@@ -217,12 +308,13 @@ export function createRouterDiscoveryPlugin(
217
308
  let prerenderTempServer: any = null;
218
309
  let prerenderNodeRegistry: Map<string, any> | null = null;
219
310
 
220
- // Clean up the temporary server when the dev server shuts down
311
+ // Clean up the temporary server and build env when the dev server shuts down
221
312
  server.httpServer?.on("close", () => {
222
313
  if (prerenderTempServer) {
223
314
  prerenderTempServer.close().catch(() => {});
224
315
  prerenderTempServer = null;
225
316
  }
317
+ releaseBuildEnv(s).catch(() => {});
226
318
  });
227
319
 
228
320
  async function getOrCreateTempServer(): Promise<any | null> {
@@ -262,6 +354,9 @@ export function createRouterDiscoveryPlugin(
262
354
  // Create a temp Node.js server to run runtime discovery and generate
263
355
  // named route types (static parser can't resolve factory calls).
264
356
  try {
357
+ // Acquire build-time env bindings for dev prerender
358
+ await acquireBuildEnv(s, viteCommand, viteMode);
359
+
265
360
  const tempRscEnv = await getOrCreateTempServer();
266
361
  if (tempRscEnv) {
267
362
  await discoverRouters(s, tempRscEnv);
@@ -278,6 +373,9 @@ export function createRouterDiscoveryPlugin(
278
373
  }
279
374
 
280
375
  try {
376
+ // Acquire build-time env bindings for dev prerender (Node.js path)
377
+ await acquireBuildEnv(s, viteCommand, viteMode);
378
+
281
379
  // Set the readiness gate BEFORE discovery so early requests
282
380
  // block until manifest is populated
283
381
  const serverMod = await rscEnv.runner.import(
@@ -413,6 +511,8 @@ export function createRouterDiscoveryPlugin(
413
511
  {},
414
512
  undefined,
415
513
  wantPassthrough,
514
+ s.resolvedBuildEnv,
515
+ true, // devMode: check getParams for passthrough routes
416
516
  );
417
517
  if (!result) continue;
418
518
  if (result.passthrough) continue;
@@ -601,6 +701,9 @@ export function createRouterDiscoveryPlugin(
601
701
  s.prerenderManifestEntries = null;
602
702
  s.staticManifestEntries = null;
603
703
 
704
+ // Acquire build-time env bindings if configured
705
+ await acquireBuildEnv(s, viteCommand, viteMode);
706
+
604
707
  let tempServer: any = null;
605
708
  // Signal to user-space code (e.g. reverse.ts) that build-time discovery
606
709
  // is active. Uses globalThis because the temp server's module runner
@@ -659,6 +762,7 @@ export function createRouterDiscoveryPlugin(
659
762
  if (tempServer) {
660
763
  await tempServer.close();
661
764
  }
765
+ await releaseBuildEnv(s);
662
766
  }
663
767
  },
664
768
 
@@ -719,33 +823,40 @@ export function createRouterDiscoveryPlugin(
719
823
  if (!s.resolvedPrerenderModules?.size && !s.resolvedStaticModules?.size)
720
824
  return;
721
825
 
826
+ // Clear maps at the start of each RSC generateBundle pass.
827
+ // Vite 6 multi-environment builds run RSC twice (analysis + production);
828
+ // clearing prevents stale/duplicate records from the analysis pass.
829
+ s.handlerChunkInfoMap.clear();
830
+ s.staticHandlerChunkInfoMap.clear();
831
+
722
832
  for (const [fileName, chunk] of Object.entries(bundle) as [
723
833
  string,
724
834
  any,
725
835
  ][]) {
726
836
  if (chunk.type !== "chunk") continue;
727
837
 
728
- // Prerender handlers chunk
729
- if (
730
- fileName.includes("__prerender-handlers") &&
731
- s.resolvedPrerenderModules?.size
732
- ) {
838
+ // Scan all chunks for handler exports (handlers may land in any chunk)
839
+ if (s.resolvedPrerenderModules?.size) {
733
840
  const handlers = extractHandlerExportsFromChunk(
734
841
  chunk.code,
735
842
  s.resolvedPrerenderModules,
736
843
  "Prerender",
737
- true,
844
+ false,
738
845
  );
739
846
  if (handlers.length > 0) {
740
- s.handlerChunkInfo = { fileName, exports: handlers };
847
+ const existing = s.handlerChunkInfoMap.get(fileName);
848
+ if (existing) {
849
+ existing.exports.push(...handlers);
850
+ } else {
851
+ s.handlerChunkInfoMap.set(fileName, {
852
+ fileName,
853
+ exports: handlers,
854
+ });
855
+ }
741
856
  }
742
857
  }
743
858
 
744
- // Static handlers chunk
745
- if (
746
- fileName.includes("__static-handlers") &&
747
- s.resolvedStaticModules?.size
748
- ) {
859
+ if (s.resolvedStaticModules?.size) {
749
860
  const handlers = extractHandlerExportsFromChunk(
750
861
  chunk.code,
751
862
  s.resolvedStaticModules,
@@ -753,7 +864,15 @@ export function createRouterDiscoveryPlugin(
753
864
  false,
754
865
  );
755
866
  if (handlers.length > 0) {
756
- s.staticHandlerChunkInfo = { fileName, exports: handlers };
867
+ const existing = s.staticHandlerChunkInfoMap.get(fileName);
868
+ if (existing) {
869
+ existing.exports.push(...handlers);
870
+ } else {
871
+ s.staticHandlerChunkInfoMap.set(fileName, {
872
+ fileName,
873
+ exports: handlers,
874
+ });
875
+ }
757
876
  }
758
877
  }
759
878
  }
@@ -59,7 +59,9 @@ export function substituteRouteParams(
59
59
 
60
60
  // Clean up slashes from omitted optional segments
61
61
  if (hadOmittedOptional) {
62
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
62
63
  result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
64
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
63
65
  }
64
66
 
65
67
  return result;