vinext 1.0.0-beta.2 → 1.0.0-beta.4

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 (174) hide show
  1. package/README.md +6 -9
  2. package/dist/build/client-build-config.js +7 -1
  3. package/dist/build/precompress.js +1 -1
  4. package/dist/build/prerender.d.ts +2 -1
  5. package/dist/build/prerender.js +12 -2
  6. package/dist/check.js +4 -0
  7. package/dist/cli-args.d.ts +2 -1
  8. package/dist/cli-args.js +10 -0
  9. package/dist/cli.js +14 -8
  10. package/dist/client/navigation-runtime.d.ts +6 -0
  11. package/dist/client/navigation-runtime.js +1 -1
  12. package/dist/config/config-matchers.js +4 -1
  13. package/dist/config/dotenv.d.ts +3 -4
  14. package/dist/config/dotenv.js +30 -22
  15. package/dist/config/next-config.d.ts +16 -5
  16. package/dist/config/next-config.js +7 -0
  17. package/dist/entries/app-browser-entry.d.ts +4 -10
  18. package/dist/entries/app-browser-entry.js +20 -4
  19. package/dist/entries/app-rsc-entry.js +21 -3
  20. package/dist/entries/app-rsc-manifest.js +19 -0
  21. package/dist/entries/pages-client-entry.js +0 -1
  22. package/dist/entries/pages-server-entry.js +13 -3
  23. package/dist/index.js +122 -49
  24. package/dist/init-cloudflare.d.ts +3 -1
  25. package/dist/init-cloudflare.js +59 -5
  26. package/dist/plugins/client-reference-dedup.js +7 -5
  27. package/dist/plugins/extensionless-dynamic-import.js +23 -18
  28. package/dist/plugins/ignore-dynamic-requests.js +3 -1
  29. package/dist/plugins/import-meta-url.js +17 -6
  30. package/dist/plugins/require-context.js +20 -15
  31. package/dist/plugins/sass.js +3 -4
  32. package/dist/plugins/transform-cache.d.ts +19 -0
  33. package/dist/plugins/transform-cache.js +36 -0
  34. package/dist/routing/app-route-graph.d.ts +14 -6
  35. package/dist/routing/app-route-graph.js +106 -16
  36. package/dist/server/api-handler.d.ts +2 -0
  37. package/dist/server/api-handler.js +7 -6
  38. package/dist/server/app-bfcache-identity.js +28 -2
  39. package/dist/server/app-browser-entry.js +175 -75
  40. package/dist/server/app-browser-navigation-controller.d.ts +23 -21
  41. package/dist/server/app-browser-navigation-controller.js +30 -5
  42. package/dist/server/app-browser-prefetch-response.d.ts +9 -0
  43. package/dist/server/app-browser-prefetch-response.js +32 -0
  44. package/dist/server/app-browser-state.d.ts +8 -3
  45. package/dist/server/app-browser-state.js +9 -3
  46. package/dist/server/app-page-cache-finalizer.js +1 -1
  47. package/dist/server/app-page-cache.d.ts +1 -1
  48. package/dist/server/app-page-cache.js +16 -2
  49. package/dist/server/app-page-dispatch.d.ts +9 -1
  50. package/dist/server/app-page-dispatch.js +37 -8
  51. package/dist/server/app-page-element-builder.d.ts +4 -1
  52. package/dist/server/app-page-element-builder.js +34 -7
  53. package/dist/server/app-page-head.d.ts +3 -3
  54. package/dist/server/app-page-head.js +58 -13
  55. package/dist/server/app-page-http-access-fallback-metadata.d.ts +2 -2
  56. package/dist/server/app-page-http-access-fallback-metadata.js +9 -1
  57. package/dist/server/app-page-probe.d.ts +4 -0
  58. package/dist/server/app-page-probe.js +6 -2
  59. package/dist/server/app-page-render.d.ts +1 -1
  60. package/dist/server/app-page-render.js +13 -3
  61. package/dist/server/app-page-request.d.ts +3 -1
  62. package/dist/server/app-page-request.js +4 -13
  63. package/dist/server/app-page-response.d.ts +2 -0
  64. package/dist/server/app-page-response.js +7 -2
  65. package/dist/server/app-page-route-wiring.d.ts +10 -2
  66. package/dist/server/app-page-route-wiring.js +269 -59
  67. package/dist/server/app-pages-bridge.d.ts +2 -1
  68. package/dist/server/app-pages-bridge.js +3 -1
  69. package/dist/server/app-route-handler-cache.js +4 -0
  70. package/dist/server/app-route-handler-dispatch.js +9 -3
  71. package/dist/server/app-route-handler-response.js +1 -1
  72. package/dist/server/app-route-module-loader.d.ts +6 -0
  73. package/dist/server/app-route-module-loader.js +3 -0
  74. package/dist/server/app-router-entry.js +6 -3
  75. package/dist/server/app-rsc-cache-busting.d.ts +7 -0
  76. package/dist/server/app-rsc-cache-busting.js +6 -0
  77. package/dist/server/app-rsc-errors.js +10 -0
  78. package/dist/server/app-rsc-handler.d.ts +2 -0
  79. package/dist/server/app-rsc-handler.js +30 -13
  80. package/dist/server/app-rsc-render-mode.d.ts +3 -2
  81. package/dist/server/app-rsc-render-mode.js +4 -1
  82. package/dist/server/app-rsc-request-normalization.js +65 -4
  83. package/dist/server/app-rsc-route-matching.d.ts +9 -0
  84. package/dist/server/app-rsc-route-matching.js +6 -0
  85. package/dist/server/app-server-action-execution.d.ts +1 -1
  86. package/dist/server/app-ssr-entry.js +4 -4
  87. package/dist/server/app-visited-response-cache.d.ts +6 -1
  88. package/dist/server/app-visited-response-cache.js +49 -1
  89. package/dist/server/cache-control.d.ts +2 -2
  90. package/dist/server/cache-control.js +2 -1
  91. package/dist/server/dev-server.d.ts +3 -1
  92. package/dist/server/dev-server.js +204 -348
  93. package/dist/server/edge-api-runtime.d.ts +10 -1
  94. package/dist/server/edge-api-runtime.js +29 -1
  95. package/dist/server/headers.d.ts +4 -2
  96. package/dist/server/headers.js +6 -3
  97. package/dist/server/isr-cache.d.ts +10 -33
  98. package/dist/server/isr-cache.js +33 -43
  99. package/dist/server/isr-decision.d.ts +2 -2
  100. package/dist/server/pages-api-route.d.ts +4 -0
  101. package/dist/server/pages-api-route.js +6 -3
  102. package/dist/server/pages-asset-tags.d.ts +1 -0
  103. package/dist/server/pages-asset-tags.js +6 -4
  104. package/dist/server/pages-dev-hydration.js +0 -1
  105. package/dist/server/pages-document-asset-props.d.ts +26 -0
  106. package/dist/server/pages-document-asset-props.js +69 -0
  107. package/dist/server/pages-get-initial-props.d.ts +1 -1
  108. package/dist/server/pages-get-initial-props.js +1 -4
  109. package/dist/server/pages-i18n.js +64 -16
  110. package/dist/server/pages-node-compat.d.ts +2 -0
  111. package/dist/server/pages-node-compat.js +10 -6
  112. package/dist/server/pages-page-data.d.ts +61 -9
  113. package/dist/server/pages-page-data.js +334 -74
  114. package/dist/server/pages-page-handler.d.ts +6 -1
  115. package/dist/server/pages-page-handler.js +119 -20
  116. package/dist/server/pages-page-response.d.ts +7 -3
  117. package/dist/server/pages-page-response.js +41 -33
  118. package/dist/server/pages-request-pipeline.d.ts +7 -0
  119. package/dist/server/pages-request-pipeline.js +5 -1
  120. package/dist/server/pages-revalidate.d.ts +1 -1
  121. package/dist/server/pages-revalidate.js +54 -8
  122. package/dist/server/pages-router-entry.d.ts +1 -0
  123. package/dist/server/pages-router-entry.js +10 -4
  124. package/dist/server/prerender-manifest.d.ts +1 -0
  125. package/dist/server/prod-server.d.ts +2 -1
  126. package/dist/server/prod-server.js +38 -13
  127. package/dist/server/revalidation-host.d.ts +11 -0
  128. package/dist/server/revalidation-host.js +19 -0
  129. package/dist/server/seed-cache.js +1 -1
  130. package/dist/server/worker-revalidation-context.d.ts +13 -0
  131. package/dist/server/worker-revalidation-context.js +30 -0
  132. package/dist/shims/app-router-scroll-state.d.ts +2 -1
  133. package/dist/shims/app-router-scroll-state.js +7 -2
  134. package/dist/shims/cache-handler.d.ts +1 -1
  135. package/dist/shims/cache-handler.js +10 -7
  136. package/dist/shims/cache-runtime.d.ts +6 -0
  137. package/dist/shims/cache-runtime.js +4 -1
  138. package/dist/shims/cache.js +1 -1
  139. package/dist/shims/document.js +15 -2
  140. package/dist/shims/error.js +1 -1
  141. package/dist/shims/fetch-cache.js +1 -1
  142. package/dist/shims/font-google-base.js +17 -8
  143. package/dist/shims/font-local.js +41 -10
  144. package/dist/shims/hash-scroll.d.ts +1 -1
  145. package/dist/shims/hash-scroll.js +2 -1
  146. package/dist/shims/headers.d.ts +10 -13
  147. package/dist/shims/headers.js +85 -36
  148. package/dist/shims/internal/cookie-serialize.d.ts +10 -9
  149. package/dist/shims/internal/cookie-serialize.js +9 -8
  150. package/dist/shims/internal/interpolate-as.d.ts +15 -1
  151. package/dist/shims/internal/interpolate-as.js +27 -2
  152. package/dist/shims/link.d.ts +2 -0
  153. package/dist/shims/link.js +77 -24
  154. package/dist/shims/metadata.d.ts +18 -34
  155. package/dist/shims/metadata.js +98 -56
  156. package/dist/shims/navigation.d.ts +18 -2
  157. package/dist/shims/navigation.js +70 -21
  158. package/dist/shims/request-context.d.ts +6 -2
  159. package/dist/shims/router.js +18 -7
  160. package/dist/shims/script.js +1 -1
  161. package/dist/shims/server.d.ts +7 -6
  162. package/dist/shims/server.js +156 -57
  163. package/dist/shims/slot.d.ts +2 -1
  164. package/dist/shims/slot.js +6 -2
  165. package/dist/shims/streamed-icons.d.ts +9 -0
  166. package/dist/shims/streamed-icons.js +33 -0
  167. package/dist/shims/unified-request-context.d.ts +21 -2
  168. package/dist/shims/unified-request-context.js +82 -1
  169. package/dist/utils/cache-control-metadata.d.ts +2 -1
  170. package/dist/utils/cache-control-metadata.js +5 -1
  171. package/dist/utils/html-limited-bots.js +1 -1
  172. package/dist/utils/protocol-headers.d.ts +7 -1
  173. package/dist/utils/protocol-headers.js +7 -1
  174. package/package.json +1 -1
@@ -16,7 +16,7 @@ function validateCloudflarePlatformSetup(context, cloudflare) {
16
16
  const projectInfo = detectProject(context.root);
17
17
  const wranglerPath = ["wrangler.jsonc", "wrangler.json"].map((fileName) => path.join(context.root, fileName)).find((candidate) => fs.existsSync(candidate));
18
18
  const wranglerCode = wranglerPath ? fs.readFileSync(wranglerPath, "utf-8") : void 0;
19
- const updatedWranglerCode = wranglerCode ? updateWranglerConfigForCloudflare(wranglerCode, cloudflare) : void 0;
19
+ const updatedWranglerCode = wranglerCode ? updateWranglerConfigForCloudflare(wranglerCode, cloudflare, { root: context.root }) : void 0;
20
20
  const imagesBinding = updatedWranglerCode ? getWranglerImagesBinding(updatedWranglerCode) : "IMAGES";
21
21
  if (context.existingViteConfigPath) updateViteConfigForCloudflare(context.existingViteConfigPath, fs.readFileSync(context.existingViteConfigPath, "utf-8"), {
22
22
  isAppRouter: context.isAppRouter,
@@ -56,7 +56,7 @@ function setupCloudflarePlatform(context, cloudflare) {
56
56
  fs.writeFileSync(path.join(context.root, "wrangler.jsonc"), generateWranglerConfig(projectInfo, cloudflare, context.today), "utf-8");
57
57
  generatedPlatformFiles.push("wrangler.jsonc");
58
58
  } else if (wranglerCode) {
59
- const updatedConfig = updateWranglerConfigForCloudflare(wranglerCode, cloudflare);
59
+ const updatedConfig = updateWranglerConfigForCloudflare(wranglerCode, cloudflare, { root: context.root });
60
60
  if (updatedConfig !== wranglerCode) {
61
61
  fs.writeFileSync(wranglerPath, updatedConfig, "utf-8");
62
62
  generatedPlatformFiles.push(path.basename(wranglerPath));
@@ -79,8 +79,19 @@ function setupCloudflarePlatform(context, cloudflare) {
79
79
  ] : []
80
80
  };
81
81
  }
82
+ /**
83
+ * `main` is what makes a Wrangler config a Worker rather than a static-assets
84
+ * project. A custom `worker/index.*` wins when present; otherwise the
85
+ * router-selected entry resolves to the App or Pages Router handler at build
86
+ * time.
87
+ */
88
+ function resolveWorkerEntry(root) {
89
+ if (fs.existsSync(path.join(root, "worker", "index.ts"))) return "./worker/index.ts";
90
+ if (fs.existsSync(path.join(root, "worker", "index.js"))) return "./worker/index.js";
91
+ return "vinext/server/fetch-handler";
92
+ }
82
93
  function generateWranglerConfig(info, options = DEFAULT_CLOUDFLARE_INIT_OPTIONS, today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]) {
83
- const workerEntry = (fs.existsSync(path.join(info.root, "worker", "index.ts")) ? "./worker/index.ts" : fs.existsSync(path.join(info.root, "worker", "index.js")) ? "./worker/index.js" : void 0) ?? "vinext/server/fetch-handler";
94
+ const workerEntry = resolveWorkerEntry(info.root);
84
95
  const config = {
85
96
  $schema: "node_modules/wrangler/config-schema.json",
86
97
  name: info.projectName,
@@ -258,13 +269,20 @@ function appendTopLevelJsonProperty(code, property) {
258
269
  const structuralBefore = stripJsonComments(before);
259
270
  return `${before}${!/,\s*$/.test(structuralBefore) && !/{\s*$/.test(structuralBefore) ? "," : ""}\n${property}\n${code.slice(closing)}`;
260
271
  }
261
- function updateWranglerConfigForCloudflare(code, options) {
272
+ function updateWranglerConfigForCloudflare(code, options, context = {}) {
273
+ let config;
262
274
  try {
263
- JSON.parse(stripJsonComments(code));
275
+ config = JSON.parse(stripJsonComments(code));
264
276
  } catch (cause) {
265
277
  throw new Error("Could not parse the existing Wrangler JSON/JSONC config.", { cause });
266
278
  }
279
+ if (Object.hasOwn(config, "pages_build_output_dir")) throw new Error("The existing Wrangler config uses \"pages_build_output_dir\", which cannot be combined with the Worker \"main\" required by vinext. Remove \"pages_build_output_dir\" and rerun vinext init.");
267
280
  let output = code;
281
+ if (!findTopLevelJsonProperty(output, "main")) {
282
+ const workerEntry = resolveWorkerEntry(context.root ?? process.cwd());
283
+ output = appendTopLevelJsonProperty(output, ` "main": ${JSON.stringify(workerEntry)}`);
284
+ }
285
+ if (!findTopLevelJsonProperty(output, "assets")) output = appendTopLevelJsonProperty(output, " \"assets\": { \"directory\": \"dist/client\", \"not_found_handling\": \"none\", \"binding\": \"ASSETS\" }");
268
286
  if (options.cdnCache === "workers-cache") {
269
287
  const cacheProperty = findTopLevelJsonProperty(output, "cache");
270
288
  if (!cacheProperty) output = appendTopLevelJsonProperty(output, " \"cache\": { \"enabled\": true }");
@@ -613,6 +631,41 @@ function endsWithCommaIgnoringWhitespaceAndComments(code) {
613
631
  function cloudflarePluginExpression(isAppRouter, binding) {
614
632
  return isAppRouter ? `${binding}({\n viteEnvironment: {\n name: "rsc",\n childEnvironments: ["ssr"],\n },\n})` : `${binding}()`;
615
633
  }
634
+ /**
635
+ * An existing bare `cloudflare()` call is left as-is by `ensurePlugins`, which
636
+ * only adds plugins that are absent. For the App Router that silently drops
637
+ * `viteEnvironment`, so the RSC environment never runs in workerd.
638
+ */
639
+ function ensureCloudflareViteEnvironment(output, config, binding, isAppRouter, code) {
640
+ if (!isAppRouter) return;
641
+ const call = findPluginCall(config, binding);
642
+ if (!call) return;
643
+ const viteEnvironment = `viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] }`;
644
+ const firstArgument = call.arguments[0];
645
+ if (!firstArgument) {
646
+ output.appendLeft(call.end - 1, `{ ${viteEnvironment} }`);
647
+ return;
648
+ }
649
+ if (firstArgument.type === "SpreadElement" || firstArgument.type !== "ObjectExpression") throw new Error("The cloudflare() plugin options must be a static object for vinext init to configure the App Router Vite environment.");
650
+ const argumentObject = firstArgument;
651
+ const existingViteEnvironment = argumentObject.properties.filter((property) => property.type === "Property" && propertyName(property) === "viteEnvironment").at(-1);
652
+ if (existingViteEnvironment) {
653
+ const propertyIndex = argumentObject.properties.lastIndexOf(existingViteEnvironment);
654
+ if (argumentObject.properties.slice(propertyIndex + 1).some((property) => property.type === "SpreadElement")) throw new Error("The cloudflare() viteEnvironment option must appear after any spread properties so vinext init can verify it.");
655
+ if (existingViteEnvironment.value.type !== "ObjectExpression") throw new Error("The cloudflare() viteEnvironment option must be a static object with name: \"rsc\" and childEnvironments containing \"ssr\".");
656
+ const environmentObject = existingViteEnvironment.value;
657
+ const nameProperties = environmentObject.properties.filter((property) => property.type === "Property" && propertyName(property) === "name");
658
+ const childEnvironmentProperties = environmentObject.properties.filter((property) => property.type === "Property" && propertyName(property) === "childEnvironments");
659
+ const name = nameProperties[0];
660
+ const childEnvironments = childEnvironmentProperties[0];
661
+ const hasAmbiguousProperties = environmentObject.properties.some((property) => property.type === "SpreadElement") || nameProperties.length !== 1 || childEnvironmentProperties.length !== 1;
662
+ const hasRequiredName = name?.value.type === "Literal" && name.value.value === "rsc";
663
+ const hasRequiredChild = childEnvironments?.value.type === "ArrayExpression" && childEnvironments.value.elements.some((element) => element?.type === "Literal" && element.value === "ssr");
664
+ if (hasAmbiguousProperties || !hasRequiredName || !hasRequiredChild) throw new Error("The cloudflare() viteEnvironment option must statically set name: \"rsc\" and include \"ssr\" in childEnvironments.");
665
+ return;
666
+ }
667
+ insertObjectProperty(output, argumentObject, `${code.slice(0, call.start).split("\n").at(-1)?.match(/^\s*/)?.[0] ?? ""} ${viteEnvironment},`, code);
668
+ }
616
669
  function findPluginCall(config, binding) {
617
670
  const plugins = findProperty(config, "plugins");
618
671
  if (!plugins || plugins.value.type !== "ArrayExpression") return void 0;
@@ -819,6 +872,7 @@ function updateViteConfigForCloudflare(filePath, code, options) {
819
872
  expression: cloudflarePluginExpression(options.isAppRouter, cloudflareBinding),
820
873
  binding: cloudflareBinding
821
874
  }], code);
875
+ ensureCloudflareViteEnvironment(output, config, cloudflareBinding, options.isAppRouter, code);
822
876
  if (existingVinextCall) if (existingVinextCall.arguments.length === 0 && (cacheAdditions.length > 0 || imageOptimizerExpression || needsPrerender)) {
823
877
  const properties = [];
824
878
  if (cacheAdditions.length > 0) properties.push(`cache: { ${cacheAdditions.map(({ name, expression }) => `${name}: ${expression}`).join(", ")} }`);
@@ -1,3 +1,4 @@
1
+ import path, { toSlash } from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
1
2
  import { isUnknownRecord } from "../utils/record.js";
2
3
  import { readFile } from "node:fs/promises";
3
4
  //#region src/plugins/client-reference-dedup.ts
@@ -156,14 +157,15 @@ function clientReferenceDedupPlugin(options = {}) {
156
157
  async handler(id, importer) {
157
158
  if (this.environment?.name !== "client") return;
158
159
  if (!importer || !importer.includes(PROXY_MARKER)) return;
159
- if (!id.startsWith("/") || !id.includes("/node_modules/")) return;
160
- const packageName = extractPackageName(id);
160
+ const normalizedId = toSlash(id);
161
+ if (!path.isAbsolute(normalizedId) || !normalizedId.includes("/node_modules/")) return;
162
+ const packageName = extractPackageName(normalizedId);
161
163
  if (!packageName) return;
162
164
  if (excludeSet.has(packageName)) return;
163
- let packageImportPromise = packageImportCache.get(id);
165
+ let packageImportPromise = packageImportCache.get(normalizedId);
164
166
  if (!packageImportPromise) {
165
- packageImportPromise = extractPackageImportSpecifier(id, readPackageJson);
166
- packageImportCache.set(id, packageImportPromise);
167
+ packageImportPromise = extractPackageImportSpecifier(normalizedId, readPackageJson);
168
+ packageImportCache.set(normalizedId, packageImportPromise);
167
169
  }
168
170
  const packageImport = await packageImportPromise;
169
171
  if (!packageImport) return;
@@ -1,4 +1,5 @@
1
1
  import { DYNAMIC_IMPORT_PRESCAN, forEachAstChild, hasRange, isAstRecord, nodeArray } from "./ast-utils.js";
2
+ import { createTransformCache } from "./transform-cache.js";
2
3
  import { parseAst } from "vite";
3
4
  import MagicString from "magic-string";
4
5
  //#region src/plugins/extensionless-dynamic-import.ts
@@ -23,6 +24,7 @@ const TRANSFORMABLE_EXTENSIONS = /* @__PURE__ */ new Set([
23
24
  ]);
24
25
  function createExtensionlessDynamicImportPlugin() {
25
26
  let moduleExtensions = MODULE_EXTENSIONS;
27
+ const cached = createTransformCache();
26
28
  return {
27
29
  name: "vinext:extensionless-dynamic-import",
28
30
  enforce: "pre",
@@ -38,28 +40,31 @@ function createExtensionlessDynamicImportPlugin() {
38
40
  code: DYNAMIC_IMPORT_PRESCAN
39
41
  },
40
42
  handler(code, id) {
41
- const lang = langForId(id);
42
- let ast;
43
- try {
44
- ast = parseAst(code, { lang });
45
- } catch {
46
- return null;
47
- }
48
- const imports = collectExtensionlessImports(ast, code, moduleExtensions);
49
- if (imports.length === 0) return null;
50
- const output = new MagicString(code);
51
- for (const dynamicImport of imports) {
52
- const source = code.slice(dynamicImport.sourceStart, dynamicImport.sourceEnd);
53
- output.overwrite(dynamicImport.start, dynamicImport.end, buildReplacement(source, dynamicImport.globPattern, dynamicImport.moduleExtensions));
54
- }
55
- return {
56
- code: output.toString(),
57
- map: output.generateMap({ hires: "boundary" })
58
- };
43
+ return cached(id, code, moduleExtensions, () => transformExtensionlessImports(code, id, moduleExtensions));
59
44
  }
60
45
  }
61
46
  };
62
47
  }
48
+ function transformExtensionlessImports(code, id, moduleExtensions) {
49
+ const lang = langForId(id);
50
+ let ast;
51
+ try {
52
+ ast = parseAst(code, { lang });
53
+ } catch {
54
+ return null;
55
+ }
56
+ const imports = collectExtensionlessImports(ast, code, moduleExtensions);
57
+ if (imports.length === 0) return null;
58
+ const output = new MagicString(code);
59
+ for (const dynamicImport of imports) {
60
+ const source = code.slice(dynamicImport.sourceStart, dynamicImport.sourceEnd);
61
+ output.overwrite(dynamicImport.start, dynamicImport.end, buildReplacement(source, dynamicImport.globPattern, dynamicImport.moduleExtensions));
62
+ }
63
+ return {
64
+ code: output.toString(),
65
+ map: output.generateMap({ hires: "boundary" })
66
+ };
67
+ }
63
68
  function langForId(id) {
64
69
  const clean = id.split("?", 1)[0];
65
70
  const dot = clean.lastIndexOf(".");
@@ -1,5 +1,6 @@
1
1
  import path, { toSlash } from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import { DYNAMIC_IMPORT_PRESCAN, collectBindingNames, forEachAstChild, hasRange, isAstRecord, isIdentifierNamed, mayContainDynamicImport, nodeArray } from "./ast-utils.js";
3
+ import { createTransformCache } from "./transform-cache.js";
3
4
  import { collectDirectScopeBindings, collectLoopScopeBindings, collectSwitchScopeBindings, collectVarScopeBindings, hasAstBinding, isFunctionNode } from "./ast-scope.js";
4
5
  import { parseAst } from "vite";
5
6
  import { fileURLToPath } from "node:url";
@@ -505,6 +506,7 @@ function transformVeryDynamicRequests(code, id) {
505
506
  };
506
507
  }
507
508
  function createIgnoreDynamicRequestsPlugin(getTranspiledPackages = () => []) {
509
+ const cached = createTransformCache();
508
510
  return {
509
511
  name: "vinext:ignore-dynamic-requests",
510
512
  enforce: "pre",
@@ -519,7 +521,7 @@ function createIgnoreDynamicRequestsPlugin(getTranspiledPackages = () => []) {
519
521
  if (!shouldTransformVeryDynamicRequests(this.environment, cleanId, getTranspiledPackages())) return null;
520
522
  const absoluteId = path.resolve(cleanId);
521
523
  if (absoluteId === VINEXT_SOURCE_ROOT || absoluteId.startsWith(`${VINEXT_SOURCE_ROOT}/`) || PLUGIN_RSC_PATH.test(absoluteId)) return null;
522
- return transformVeryDynamicRequests(code, id);
524
+ return cached(id, code, void 0, () => transformVeryDynamicRequests(code, id));
523
525
  }
524
526
  }
525
527
  };
@@ -19,6 +19,7 @@ const TRANSFORMABLE_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
19
19
  function createImportMetaUrlPlugin(options) {
20
20
  let rootPaths;
21
21
  let outputDirs = [];
22
+ const transformCache = /* @__PURE__ */ new Map();
22
23
  function getRootPaths() {
23
24
  const root = options.getRoot();
24
25
  if (!root) return rootPaths;
@@ -46,12 +47,22 @@ function createImportMetaUrlPlugin(options) {
46
47
  if (!paths) return null;
47
48
  const canonicalId = transformableModuleCanonicalId(cleanModuleId(id), paths);
48
49
  if (!canonicalId) return null;
49
- const rewritten = rewriteCanonicalSourceIdentity(code, canonicalId, paths, this.environment?.name === "client" ? "client" : "server");
50
- if (!rewritten) return null;
51
- return {
52
- code: rewritten.code,
53
- map: rewritten.map
54
- };
50
+ const environment = this.environment?.name === "client" ? "client" : "server";
51
+ let entry = transformCache.get(id);
52
+ if (!entry || entry.source !== code || entry.canonicalRoot !== paths.canonicalRoot || entry.canonicalId !== canonicalId) {
53
+ entry = {
54
+ source: code,
55
+ canonicalRoot: paths.canonicalRoot,
56
+ canonicalId,
57
+ results: /* @__PURE__ */ new Map()
58
+ };
59
+ transformCache.set(id, entry);
60
+ }
61
+ const cached = entry.results.get(environment);
62
+ if (cached) return cached.value;
63
+ const value = rewriteCanonicalSourceIdentity(code, canonicalId, paths, environment);
64
+ entry.results.set(environment, { value });
65
+ return value;
55
66
  }
56
67
  }
57
68
  };
@@ -1,4 +1,5 @@
1
1
  import { forEachAstChild, hasRange, isAstRecord, nodeArray } from "./ast-utils.js";
2
+ import { createTransformCache } from "./transform-cache.js";
2
3
  import { parseAst } from "vite";
3
4
  import MagicString from "magic-string";
4
5
  //#region src/plugins/require-context.ts
@@ -13,6 +14,7 @@ const TRANSFORMABLE_EXTENSIONS = /* @__PURE__ */ new Set([
13
14
  ".cts"
14
15
  ]);
15
16
  function createRequireContextPlugin() {
17
+ const cached = createTransformCache();
16
18
  return {
17
19
  name: "vinext:require-context",
18
20
  enforce: "pre",
@@ -22,25 +24,28 @@ function createRequireContextPlugin() {
22
24
  code: /\brequire\b[\s\S]*\.context/
23
25
  },
24
26
  handler(code, id) {
25
- const lang = langForId(id);
26
- let ast;
27
- try {
28
- ast = parseAst(code, { lang });
29
- } catch {
30
- return null;
31
- }
32
- const calls = collectRequireContextCalls(ast);
33
- if (calls.length === 0) return null;
34
- const output = new MagicString(code);
35
- for (const call of calls) output.overwrite(call.range.start, call.range.end, buildReplacement(call));
36
- return {
37
- code: output.toString(),
38
- map: output.generateMap({ hires: "boundary" })
39
- };
27
+ return cached(id, code, void 0, () => transformRequireContext(code, id));
40
28
  }
41
29
  }
42
30
  };
43
31
  }
32
+ function transformRequireContext(code, id) {
33
+ const lang = langForId(id);
34
+ let ast;
35
+ try {
36
+ ast = parseAst(code, { lang });
37
+ } catch {
38
+ return null;
39
+ }
40
+ const calls = collectRequireContextCalls(ast);
41
+ if (calls.length === 0) return null;
42
+ const output = new MagicString(code);
43
+ for (const call of calls) output.overwrite(call.range.start, call.range.end, buildReplacement(call));
44
+ return {
45
+ code: output.toString(),
46
+ map: output.generateMap({ hires: "boundary" })
47
+ };
48
+ }
44
49
  function langForId(id) {
45
50
  const clean = id.split("?", 1)[0];
46
51
  const dot = clean.lastIndexOf(".");
@@ -3,7 +3,7 @@ import { markCssUrlAssetReferences, rebaseCssUrlAssetReferences } from "../build
3
3
  import { createRequire } from "node:module";
4
4
  import fs from "node:fs";
5
5
  import { preprocessCSS } from "vite";
6
- import { pathToFileURL } from "node:url";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  //#region src/plugins/sass.ts
8
8
  /**
9
9
  * Map a Next.js `sassOptions` object onto Vite's
@@ -130,8 +130,7 @@ function createSassCssUrlAssetImporter() {
130
130
  if (!fs.statSync(candidate, { throwIfNoEntry: false })?.isFile()) continue;
131
131
  const prepared = prepareStylesheet(candidate, entryDirectory);
132
132
  if (prepared === null) return match;
133
- const canonicalUrl = pathToFileURL(candidate);
134
- markedStylesheets.set(canonicalUrl.href, prepared);
133
+ markedStylesheets.set(candidate, prepared);
135
134
  const namespace = rule === "use" && !/\bas\s+(?:\*|[-\w]+)/.test(suffix) ? ` as ${deriveSassNamespace(importUrl)}` : "";
136
135
  return `${prefix}${quote}${importUrlPrefix}${encodeURIComponent(candidate)}${quote}${namespace}${suffix}`;
137
136
  }
@@ -143,7 +142,7 @@ function createSassCssUrlAssetImporter() {
143
142
  return pathToFileURL(decodeURIComponent(url.slice(21)));
144
143
  },
145
144
  load(canonicalUrl) {
146
- return markedStylesheets.get(canonicalUrl.href) ?? null;
145
+ return markedStylesheets.get(toSlash(fileURLToPath(canonicalUrl))) ?? null;
147
146
  },
148
147
  rewriteImports
149
148
  };
@@ -0,0 +1,19 @@
1
+ //#region src/plugins/transform-cache.d.ts
2
+ /**
3
+ * Memoizes a pure per-module transform across the repeated build passes
4
+ * (scan client/server references + rsc/client/ssr builds), which feed the
5
+ * same module source through the same transform up to five times.
6
+ *
7
+ * Entries are keyed by module id and replaced when the source changes, so dev
8
+ * rebuilds retain at most one source version per id. `variant` distinguishes
9
+ * results that also depend on an environment-derived input (e.g. the
10
+ * typeof-window replacement); callers must pass a primitive or stable
11
+ * reference. Cached values include `null` ("no transform applies") so the
12
+ * negative case is not recomputed either.
13
+ *
14
+ * Only safe for transforms whose result depends solely on (id, source,
15
+ * variant) — no filesystem or plugin-state reads inside `compute`.
16
+ */
17
+ declare function createTransformCache<V, R>(): (id: string, source: string, variant: V, compute: () => R) => R;
18
+ //#endregion
19
+ export { createTransformCache };
@@ -0,0 +1,36 @@
1
+ //#region src/plugins/transform-cache.ts
2
+ /**
3
+ * Memoizes a pure per-module transform across the repeated build passes
4
+ * (scan client/server references + rsc/client/ssr builds), which feed the
5
+ * same module source through the same transform up to five times.
6
+ *
7
+ * Entries are keyed by module id and replaced when the source changes, so dev
8
+ * rebuilds retain at most one source version per id. `variant` distinguishes
9
+ * results that also depend on an environment-derived input (e.g. the
10
+ * typeof-window replacement); callers must pass a primitive or stable
11
+ * reference. Cached values include `null` ("no transform applies") so the
12
+ * negative case is not recomputed either.
13
+ *
14
+ * Only safe for transforms whose result depends solely on (id, source,
15
+ * variant) — no filesystem or plugin-state reads inside `compute`.
16
+ */
17
+ function createTransformCache() {
18
+ const cache = /* @__PURE__ */ new Map();
19
+ return (id, source, variant, compute) => {
20
+ let entry = cache.get(id);
21
+ if (entry?.source !== source) {
22
+ entry = {
23
+ source,
24
+ results: /* @__PURE__ */ new Map()
25
+ };
26
+ cache.set(id, entry);
27
+ }
28
+ const boxed = entry.results.get(variant);
29
+ if (boxed) return boxed.value;
30
+ const value = compute();
31
+ entry.results.set(variant, { value });
32
+ return value;
33
+ };
34
+ }
35
+ //#endregion
36
+ export { createTransformCache };
@@ -24,7 +24,9 @@ type InterceptingRoute = {
24
24
  pagePath: string; /** Filesystem segments from app/ root to the intercepting page directory. */
25
25
  sourcePageSegments?: string[]; /** Absolute layout paths inside the intercepting route tree, outermost to innermost */
26
26
  layoutPaths: string[]; /** Normalized branch segments accumulated at each intercept layout. */
27
- layoutSegments?: string[][]; /** Full normalized interception branch segments through the page. */
27
+ layoutSegments?: string[][]; /** Loading conventions inside the intercepted branch, outermost to innermost. */
28
+ loadingPaths?: string[]; /** Tree positions for loadingPaths in the normalized interception branch. */
29
+ loadingTreePositions?: number[]; /** Full normalized interception branch segments through the page. */
28
30
  branchSegments?: string[]; /** Nearest not-found convention inside the interception branch. */
29
31
  notFoundPath?: string | null; /** Slot-root-relative branch segments used to scope not-found params. */
30
32
  notFoundBranchSegments?: string[]; /** Tree position relative to the normalized interception branch. */
@@ -41,14 +43,17 @@ type ParallelSlot = {
41
43
  key: string; /** Slot name (e.g. "team" from @team) */
42
44
  name: string; /** Absolute path to the @slot directory that owns this slot. Internal routing metadata. */
43
45
  ownerDir: string; /** Stable tree path for the directory whose layout owns this slot. */
44
- ownerTreePath: string; /** Whether the slot owner directory declares its own page component. */
46
+ ownerTreePath: string; /** Directory depth of the slot owner from app/ root. */
47
+ ownerTreePosition?: number; /** Whether the slot owner directory declares its own page component. */
45
48
  hasPage: boolean; /** Absolute path to the slot's page component */
46
49
  pagePath: string | null; /** Absolute path to the slot's default.tsx fallback */
47
50
  defaultPath: string | null; /** Absolute path to the slot's layout component (wraps slot content) */
48
51
  layoutPath: string | null; /** Nested active-branch layouts whose exports contribute route config. */
49
52
  configLayoutPaths?: string[]; /** Tree positions of configLayoutPaths relative to the slot root. */
50
53
  configLayoutTreePositions?: number[]; /** Absolute path to the slot's loading component */
51
- loadingPath: string | null; /** Absolute path to the slot's error component */
54
+ loadingPath: string | null; /** Per-segment loading components from the slot root to its active page. */
55
+ loadingPaths?: string[]; /** Slot-root-relative tree positions aligned with loadingPaths. */
56
+ loadingTreePositions?: number[]; /** Absolute path to the slot's error component */
52
57
  errorPath: string | null; /** Nearest not-found convention for the slot's active branch. */
53
58
  notFoundPath?: string | null; /** Tree position of that convention relative to the slot root. */
54
59
  notFoundTreePosition?: number | null; /** Intercepting routes within this slot */
@@ -99,8 +104,10 @@ type AppRoute = {
99
104
  * On soft-nav, the intercepting page replaces the entire page response.
100
105
  * Empty array when there are no sibling-style interception markers.
101
106
  */
102
- siblingIntercepts: InterceptingRoute[]; /** Loading component path */
103
- loadingPath: string | null; /** Error component path (leaf directory only) */
107
+ siblingIntercepts: InterceptingRoute[]; /** Loading component path (leaf directory only) */
108
+ loadingPath: string | null; /** Per-segment loading component paths, aligned with loadingTreePositions. */
109
+ loadingPaths?: string[]; /** Tree position (directory depth from app/ root) for each loading boundary. */
110
+ loadingTreePositions?: number[]; /** Error component path (leaf directory only) */
104
111
  errorPath: string | null;
105
112
  /**
106
113
  * Per-layout error boundary paths, aligned with the layouts array.
@@ -306,6 +313,7 @@ declare function buildAppRouteGraph(appDir: string, matcher: ValidFileMatcher):
306
313
  routeManifest: RouteManifest;
307
314
  }>;
308
315
  declare function computeRootParamNames(routeSegments: readonly string[], layoutTreePositions: readonly number[]): string[];
316
+ declare function patternsStructurallyEquivalent(a: readonly string[], b: readonly string[]): boolean;
309
317
  /**
310
318
  * Find the best route to attach a sibling intercept to, given the directory
311
319
  * that contains the interception marker.
@@ -359,4 +367,4 @@ declare function computeAppRouteStaticSiblings(allRoutes: readonly {
359
367
  patternParts?: readonly string[] | null;
360
368
  }): string[];
361
369
  //#endregion
362
- export { AppRoute, AppRouteGraphRoute, AppRouteSemanticIds, GraphVersion, RootBoundaryId, RouteManifest, RouteManifestInterception, RouteManifestRootBoundary, RouteManifestRoute, RouteManifestSlotBinding, StaticSegmentGraph, buildAppRouteGraph, computeAppRouteStaticSiblings, computeRootParamNames, convertSegmentsToRouteParts, findOwnerRouteForDir, isInvisibleSegment };
370
+ export { AppRoute, AppRouteGraphRoute, AppRouteSemanticIds, GraphVersion, RootBoundaryId, RouteManifest, RouteManifestInterception, RouteManifestRootBoundary, RouteManifestRoute, RouteManifestSlotBinding, StaticSegmentGraph, buildAppRouteGraph, computeAppRouteStaticSiblings, computeRootParamNames, convertSegmentsToRouteParts, findOwnerRouteForDir, isInvisibleSegment, patternsStructurallyEquivalent };