vinext 0.1.2 → 0.1.3

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 (199) hide show
  1. package/dist/build/prerender.d.ts +9 -1
  2. package/dist/build/prerender.js +41 -12
  3. package/dist/build/run-prerender.d.ts +10 -2
  4. package/dist/build/run-prerender.js +15 -1
  5. package/dist/client/app-nav-failure-handler.d.ts +8 -0
  6. package/dist/client/app-nav-failure-handler.js +44 -0
  7. package/dist/client/vinext-next-data.d.ts +18 -1
  8. package/dist/client/window-next.d.ts +2 -1
  9. package/dist/client/window-next.js +12 -1
  10. package/dist/cloudflare/src/cache/cdn-adapter.runtime.js +6 -1
  11. package/dist/config/config-matchers.js +73 -14
  12. package/dist/config/next-config.d.ts +46 -4
  13. package/dist/config/next-config.js +147 -48
  14. package/dist/deploy.d.ts +30 -11
  15. package/dist/deploy.js +180 -99
  16. package/dist/entries/app-browser-entry.d.ts +9 -3
  17. package/dist/entries/app-browser-entry.js +21 -3
  18. package/dist/entries/app-rsc-entry.d.ts +2 -0
  19. package/dist/entries/app-rsc-entry.js +64 -5
  20. package/dist/entries/app-rsc-manifest.js +2 -0
  21. package/dist/entries/app-ssr-entry.js +1 -1
  22. package/dist/entries/pages-client-entry.js +53 -8
  23. package/dist/entries/pages-server-entry.js +41 -5
  24. package/dist/index.js +200 -62
  25. package/dist/plugins/extensionless-dynamic-import.d.ts +6 -0
  26. package/dist/plugins/extensionless-dynamic-import.js +152 -0
  27. package/dist/plugins/optimize-imports.d.ts +2 -1
  28. package/dist/plugins/optimize-imports.js +11 -9
  29. package/dist/plugins/postcss.js +7 -7
  30. package/dist/plugins/typeof-window.d.ts +14 -0
  31. package/dist/plugins/typeof-window.js +150 -0
  32. package/dist/routing/app-route-graph.d.ts +2 -1
  33. package/dist/routing/app-route-graph.js +44 -14
  34. package/dist/routing/file-matcher.d.ts +10 -1
  35. package/dist/routing/file-matcher.js +22 -1
  36. package/dist/routing/pages-router.js +3 -3
  37. package/dist/routing/utils.d.ts +35 -6
  38. package/dist/routing/utils.js +59 -7
  39. package/dist/server/api-handler.d.ts +6 -1
  40. package/dist/server/api-handler.js +21 -15
  41. package/dist/server/app-browser-action-result.d.ts +19 -6
  42. package/dist/server/app-browser-action-result.js +19 -10
  43. package/dist/server/app-browser-entry.js +167 -90
  44. package/dist/server/app-browser-error.d.ts +10 -6
  45. package/dist/server/app-browser-error.js +43 -8
  46. package/dist/server/app-browser-hydration.d.ts +2 -0
  47. package/dist/server/app-browser-hydration.js +1 -0
  48. package/dist/server/app-browser-navigation-controller.d.ts +4 -2
  49. package/dist/server/app-browser-navigation-controller.js +23 -2
  50. package/dist/server/app-browser-server-action-navigation.d.ts +6 -0
  51. package/dist/server/app-browser-server-action-navigation.js +9 -0
  52. package/dist/server/app-browser-stream.js +86 -43
  53. package/dist/server/app-elements-wire.d.ts +6 -1
  54. package/dist/server/app-elements-wire.js +14 -4
  55. package/dist/server/app-elements.d.ts +2 -2
  56. package/dist/server/app-elements.js +2 -2
  57. package/dist/server/app-fallback-renderer.d.ts +1 -0
  58. package/dist/server/app-fallback-renderer.js +3 -1
  59. package/dist/server/app-optimistic-routing.js +2 -2
  60. package/dist/server/app-page-boundary-render.d.ts +1 -0
  61. package/dist/server/app-page-boundary-render.js +27 -14
  62. package/dist/server/app-page-cache-render.d.ts +53 -0
  63. package/dist/server/app-page-cache-render.js +91 -0
  64. package/dist/server/app-page-cache.d.ts +16 -2
  65. package/dist/server/app-page-cache.js +62 -1
  66. package/dist/server/app-page-dispatch.d.ts +26 -0
  67. package/dist/server/app-page-dispatch.js +149 -92
  68. package/dist/server/app-page-element-builder.d.ts +1 -0
  69. package/dist/server/app-page-element-builder.js +5 -2
  70. package/dist/server/app-page-execution.d.ts +6 -1
  71. package/dist/server/app-page-execution.js +21 -1
  72. package/dist/server/app-page-probe.d.ts +1 -0
  73. package/dist/server/app-page-probe.js +4 -0
  74. package/dist/server/app-page-render-observation.d.ts +3 -1
  75. package/dist/server/app-page-render-observation.js +17 -1
  76. package/dist/server/app-page-render.d.ts +12 -1
  77. package/dist/server/app-page-render.js +42 -4
  78. package/dist/server/app-page-request.d.ts +2 -0
  79. package/dist/server/app-page-request.js +2 -1
  80. package/dist/server/app-page-route-wiring.d.ts +3 -1
  81. package/dist/server/app-page-route-wiring.js +14 -5
  82. package/dist/server/app-page-stream.d.ts +15 -3
  83. package/dist/server/app-page-stream.js +11 -5
  84. package/dist/server/app-pages-bridge.d.ts +18 -0
  85. package/dist/server/app-pages-bridge.js +22 -5
  86. package/dist/server/app-ppr-fallback-shell-render.d.ts +17 -0
  87. package/dist/server/app-ppr-fallback-shell-render.js +26 -0
  88. package/dist/server/app-ppr-fallback-shell.d.ts +13 -1
  89. package/dist/server/app-ppr-fallback-shell.js +8 -1
  90. package/dist/server/app-route-handler-dispatch.js +9 -2
  91. package/dist/server/app-route-handler-policy.d.ts +1 -0
  92. package/dist/server/app-router-entry.js +5 -0
  93. package/dist/server/app-rsc-cache-busting.js +2 -0
  94. package/dist/server/app-rsc-handler.d.ts +25 -0
  95. package/dist/server/app-rsc-handler.js +154 -54
  96. package/dist/server/app-rsc-route-matching.d.ts +3 -0
  97. package/dist/server/app-rsc-route-matching.js +2 -0
  98. package/dist/server/app-segment-config.d.ts +9 -1
  99. package/dist/server/app-segment-config.js +12 -3
  100. package/dist/server/app-server-action-execution.d.ts +1 -0
  101. package/dist/server/app-server-action-execution.js +42 -13
  102. package/dist/server/app-ssr-entry.d.ts +2 -0
  103. package/dist/server/app-ssr-entry.js +83 -10
  104. package/dist/server/cache-control.js +4 -0
  105. package/dist/server/dev-server.d.ts +2 -2
  106. package/dist/server/dev-server.js +244 -51
  107. package/dist/server/hybrid-route-priority.d.ts +22 -0
  108. package/dist/server/hybrid-route-priority.js +33 -0
  109. package/dist/server/image-optimization.d.ts +18 -9
  110. package/dist/server/image-optimization.js +37 -23
  111. package/dist/server/implicit-tags.d.ts +2 -1
  112. package/dist/server/implicit-tags.js +4 -1
  113. package/dist/server/navigation-planner.d.ts +133 -30
  114. package/dist/server/navigation-planner.js +114 -0
  115. package/dist/server/navigation-trace.d.ts +8 -1
  116. package/dist/server/navigation-trace.js +8 -1
  117. package/dist/server/pages-api-route.d.ts +6 -0
  118. package/dist/server/pages-api-route.js +13 -2
  119. package/dist/server/pages-asset-tags.d.ts +2 -1
  120. package/dist/server/pages-asset-tags.js +6 -2
  121. package/dist/server/pages-data-route.d.ts +8 -1
  122. package/dist/server/pages-data-route.js +11 -2
  123. package/dist/server/pages-get-initial-props.d.ts +54 -4
  124. package/dist/server/pages-get-initial-props.js +43 -1
  125. package/dist/server/pages-node-compat.js +2 -2
  126. package/dist/server/pages-page-data.d.ts +11 -2
  127. package/dist/server/pages-page-data.js +204 -33
  128. package/dist/server/pages-page-handler.d.ts +4 -2
  129. package/dist/server/pages-page-handler.js +59 -22
  130. package/dist/server/pages-page-response.d.ts +2 -1
  131. package/dist/server/pages-page-response.js +7 -4
  132. package/dist/server/pages-request-pipeline.d.ts +1 -0
  133. package/dist/server/pages-request-pipeline.js +73 -36
  134. package/dist/server/pregenerated-concrete-paths.d.ts +1 -17
  135. package/dist/server/pregenerated-concrete-paths.js +2 -19
  136. package/dist/server/prerender-manifest.d.ts +33 -0
  137. package/dist/server/prerender-manifest.js +54 -0
  138. package/dist/server/prerender-route-params.d.ts +1 -2
  139. package/dist/server/prod-server.js +9 -3
  140. package/dist/server/request-pipeline.d.ts +3 -15
  141. package/dist/server/request-pipeline.js +58 -47
  142. package/dist/server/rsc-stream-hints.d.ts +5 -1
  143. package/dist/server/rsc-stream-hints.js +6 -1
  144. package/dist/server/seed-cache.js +10 -18
  145. package/dist/shims/app-router-scroll-state.d.ts +3 -1
  146. package/dist/shims/app-router-scroll-state.js +14 -2
  147. package/dist/shims/app-router-scroll.d.ts +3 -0
  148. package/dist/shims/app-router-scroll.js +28 -18
  149. package/dist/shims/cache-runtime.js +3 -2
  150. package/dist/shims/cache.d.ts +1 -0
  151. package/dist/shims/cache.js +1 -1
  152. package/dist/shims/cdn-cache.d.ts +5 -5
  153. package/dist/shims/dynamic-preload-chunks.js +6 -4
  154. package/dist/shims/error-boundary.d.ts +2 -0
  155. package/dist/shims/error-boundary.js +7 -0
  156. package/dist/shims/error.js +3 -2
  157. package/dist/shims/error.react-server.d.ts +9 -0
  158. package/dist/shims/error.react-server.js +6 -0
  159. package/dist/shims/fetch-cache.d.ts +3 -1
  160. package/dist/shims/fetch-cache.js +45 -20
  161. package/dist/shims/hash-scroll.js +6 -1
  162. package/dist/shims/headers.js +29 -4
  163. package/dist/shims/internal/als-registry.js +28 -1
  164. package/dist/shims/internal/app-route-detection.js +8 -17
  165. package/dist/shims/internal/hybrid-client-route-owner.d.ts +31 -0
  166. package/dist/shims/internal/hybrid-client-route-owner.js +143 -0
  167. package/dist/shims/internal/navigation-untracked.d.ts +35 -0
  168. package/dist/shims/internal/navigation-untracked.js +55 -0
  169. package/dist/shims/internal/pages-data-target.d.ts +7 -2
  170. package/dist/shims/internal/pages-data-target.js +17 -8
  171. package/dist/shims/internal/pages-router-accessor.d.ts +19 -0
  172. package/dist/shims/internal/pages-router-accessor.js +13 -0
  173. package/dist/shims/internal/router-context.d.ts +2 -1
  174. package/dist/shims/internal/router-context.js +3 -1
  175. package/dist/shims/link.js +12 -5
  176. package/dist/shims/navigation.d.ts +8 -2
  177. package/dist/shims/navigation.js +61 -31
  178. package/dist/shims/ppr-fallback-shell.d.ts +5 -1
  179. package/dist/shims/ppr-fallback-shell.js +28 -7
  180. package/dist/shims/router.d.ts +13 -2
  181. package/dist/shims/router.js +419 -128
  182. package/dist/shims/server.d.ts +16 -1
  183. package/dist/shims/server.js +44 -12
  184. package/dist/shims/unified-request-context.js +1 -0
  185. package/dist/utils/built-asset-url.d.ts +4 -0
  186. package/dist/utils/built-asset-url.js +11 -0
  187. package/dist/utils/commonjs-loader.d.ts +16 -0
  188. package/dist/utils/commonjs-loader.js +100 -0
  189. package/dist/utils/deployment-id.d.ts +8 -0
  190. package/dist/utils/deployment-id.js +22 -0
  191. package/dist/utils/html-limited-bots.d.ts +18 -1
  192. package/dist/utils/html-limited-bots.js +23 -1
  193. package/dist/utils/parse-cookie.d.ts +13 -0
  194. package/dist/utils/parse-cookie.js +52 -0
  195. package/dist/utils/path.d.ts +7 -1
  196. package/dist/utils/path.js +9 -1
  197. package/package.json +2 -2
  198. package/dist/shims/internal/parse-cookie-header.d.ts +0 -14
  199. package/dist/shims/internal/parse-cookie-header.js +0 -30
@@ -0,0 +1,152 @@
1
+ import { forEachAstChild, hasRange, isAstRecord, nodeArray } from "./ast-utils.js";
2
+ import { parseAst } from "vite";
3
+ import MagicString from "magic-string";
4
+ //#region src/plugins/extensionless-dynamic-import.ts
5
+ const MODULE_EXTENSIONS = [
6
+ ".mjs",
7
+ ".js",
8
+ ".mts",
9
+ ".ts",
10
+ ".jsx",
11
+ ".tsx",
12
+ ".json"
13
+ ];
14
+ const TRANSFORMABLE_EXTENSIONS = new Set([
15
+ ".mjs",
16
+ ".js",
17
+ ".mts",
18
+ ".ts",
19
+ ".jsx",
20
+ ".tsx",
21
+ ".cjs",
22
+ ".cts"
23
+ ]);
24
+ function createExtensionlessDynamicImportPlugin() {
25
+ let moduleExtensions = MODULE_EXTENSIONS;
26
+ return {
27
+ name: "vinext:extensionless-dynamic-import",
28
+ enforce: "pre",
29
+ configResolved(config) {
30
+ moduleExtensions = getModuleExtensions(config);
31
+ },
32
+ transform(code, id) {
33
+ if (!/\bimport\s*\(/.test(code)) return null;
34
+ if (isDependencyId(id)) return null;
35
+ const lang = langForId(id);
36
+ if (!lang) return null;
37
+ let ast;
38
+ try {
39
+ ast = parseAst(code, { lang });
40
+ } catch {
41
+ return null;
42
+ }
43
+ const imports = collectExtensionlessImports(ast, code, moduleExtensions);
44
+ if (imports.length === 0) return null;
45
+ const output = new MagicString(code);
46
+ for (const dynamicImport of imports) {
47
+ const source = code.slice(dynamicImport.sourceStart, dynamicImport.sourceEnd);
48
+ output.overwrite(dynamicImport.start, dynamicImport.end, buildReplacement(source, dynamicImport.globPattern, dynamicImport.moduleExtensions));
49
+ }
50
+ return {
51
+ code: output.toString(),
52
+ map: output.generateMap({ hires: "boundary" })
53
+ };
54
+ }
55
+ };
56
+ }
57
+ function langForId(id) {
58
+ const clean = id.split("?", 1)[0];
59
+ const dot = clean.lastIndexOf(".");
60
+ if (dot < 0) return null;
61
+ const ext = clean.slice(dot).toLowerCase();
62
+ if (!TRANSFORMABLE_EXTENSIONS.has(ext)) return null;
63
+ if (ext === ".ts" || ext === ".mts" || ext === ".cts") return "ts";
64
+ if (ext === ".tsx") return "tsx";
65
+ return "jsx";
66
+ }
67
+ function isDependencyId(id) {
68
+ return id.split("?", 1)[0].replaceAll("\\", "/").includes("/node_modules/");
69
+ }
70
+ function collectExtensionlessImports(ast, code, moduleExtensions) {
71
+ const imports = [];
72
+ function visit(value) {
73
+ if (!isAstRecord(value)) return;
74
+ const parsed = parseExtensionlessImport(value, code, moduleExtensions);
75
+ if (parsed) {
76
+ imports.push(parsed);
77
+ return;
78
+ }
79
+ forEachAstChild(value, visit);
80
+ }
81
+ visit(ast);
82
+ return imports;
83
+ }
84
+ function parseExtensionlessImport(node, code, moduleExtensions) {
85
+ if (node.type !== "ImportExpression" || !hasRange(node)) return null;
86
+ if (node.options != null) return null;
87
+ const source = node.source;
88
+ if (!isAstRecord(source) || source.type !== "TemplateLiteral" || !hasRange(source)) return null;
89
+ if (nodeArray(source.expressions).length === 0) return null;
90
+ const quasiTexts = nodeArray(source.quasis).map(templateElementText);
91
+ if (quasiTexts.some((text) => text == null)) return null;
92
+ const texts = quasiTexts;
93
+ const first = texts[0];
94
+ if (!isImportPrefix(code.slice(node.start, source.start))) return null;
95
+ if (!(first.startsWith("./") || first.startsWith("../"))) return null;
96
+ if (texts.some((text) => /[*?[\]{}()!?#]/.test(text))) return null;
97
+ if (texts.slice(1).some((text) => text.includes("."))) return null;
98
+ const directoryEnd = first.lastIndexOf("/") + 1;
99
+ const directory = first.slice(0, directoryEnd);
100
+ const filenamePrefix = first.slice(directoryEnd);
101
+ if (filenamePrefix.includes(".")) return null;
102
+ return {
103
+ start: node.start,
104
+ end: node.end,
105
+ sourceStart: source.start,
106
+ sourceEnd: source.end,
107
+ globPattern: filenamePrefix.length > 0 ? [`${first}*`, `${first}*/**/*`] : `${directory}**/*`,
108
+ moduleExtensions
109
+ };
110
+ }
111
+ function isImportPrefix(value) {
112
+ const prefix = value.match(/^import\s*\(/)?.[0];
113
+ if (!prefix) return false;
114
+ let index = prefix.length;
115
+ while (index < value.length) {
116
+ const char = value[index];
117
+ if (/\s/.test(char)) {
118
+ index += 1;
119
+ continue;
120
+ }
121
+ if (value.startsWith("/*", index)) {
122
+ const commentEnd = value.indexOf("*/", index + 2);
123
+ if (commentEnd < 0) return false;
124
+ index = commentEnd + 2;
125
+ continue;
126
+ }
127
+ if (value.startsWith("//", index)) {
128
+ const lineEnd = value.indexOf("\n", index + 2);
129
+ if (lineEnd < 0) return true;
130
+ index = lineEnd + 1;
131
+ continue;
132
+ }
133
+ return false;
134
+ }
135
+ return true;
136
+ }
137
+ function templateElementText(value) {
138
+ if (!isAstRecord(value) || value.type !== "TemplateElement") return null;
139
+ const templateValue = value.value;
140
+ if (typeof templateValue !== "object" || templateValue === null) return null;
141
+ const cooked = Reflect.get(templateValue, "cooked");
142
+ return typeof cooked === "string" ? cooked : null;
143
+ }
144
+ function getModuleExtensions(config) {
145
+ return config.resolve.extensions.filter((extension) => extension !== ".node");
146
+ }
147
+ function buildReplacement(source, globPattern, moduleExtensions) {
148
+ const extensions = JSON.stringify(moduleExtensions);
149
+ return `((__vinextPath, __vinextModules = import.meta.glob(${JSON.stringify(globPattern)}), __vinextExtensions = ${extensions}) => { const __vinextLoader = __vinextModules[__vinextPath] ?? __vinextExtensions.map((__vinextExtension) => __vinextModules[__vinextPath + __vinextExtension]).find(Boolean) ?? __vinextExtensions.map((__vinextExtension) => __vinextModules[__vinextPath + "/index" + __vinextExtension]).find(Boolean); return __vinextLoader ? __vinextLoader() : Promise.reject(new Error("Cannot find module '" + __vinextPath + "'")); })(${source})`;
150
+ }
151
+ //#endregion
152
+ export { createExtensionlessDynamicImportPlugin };
@@ -14,6 +14,7 @@ type BarrelExportMap = Map<string, BarrelExportEntry>;
14
14
  * @see https://github.com/vercel/next.js/blob/9c31bbdaa/packages/next/src/server/config.ts#L1301
15
15
  */
16
16
  declare const DEFAULT_OPTIMIZE_PACKAGES: string[];
17
+ declare function createOptimizedImportSourceMatcher(packages: Iterable<string>): (code: string) => boolean;
17
18
  /**
18
19
  * Build a map of exported names → source sub-module for a barrel package.
19
20
  *
@@ -38,4 +39,4 @@ declare function buildBarrelExportMap(entryPath: string | null, readFile: (filep
38
39
  */
39
40
  declare function createOptimizeImportsPlugin(getNextConfig: () => ResolvedNextConfig | undefined, getRoot: () => string): Plugin;
40
41
  //#endregion
41
- export { DEFAULT_OPTIMIZE_PACKAGES, buildBarrelExportMap, createOptimizeImportsPlugin };
42
+ export { DEFAULT_OPTIMIZE_PACKAGES, buildBarrelExportMap, createOptimizeImportsPlugin, createOptimizedImportSourceMatcher };
@@ -1,4 +1,5 @@
1
1
  import { normalizePathSeparators } from "../utils/path.js";
2
+ import { escapeRegExp } from "../utils/regex.js";
2
3
  import { getAstName } from "./ast-utils.js";
3
4
  import { createRequire } from "node:module";
4
5
  import path from "node:path";
@@ -100,6 +101,12 @@ const DEFAULT_OPTIMIZE_PACKAGES = [
100
101
  "react-icons/wi",
101
102
  "radix-ui"
102
103
  ];
104
+ function createOptimizedImportSourceMatcher(packages) {
105
+ const pattern = [...packages].map(escapeRegExp).join("|");
106
+ if (!pattern) return () => false;
107
+ const importFromPattern = new RegExp(String.raw`(?:^|[;}\n\r])\s*import(?!\s*\()(?:(?!\bfrom\b)[\s\S])*?\bfrom\s*["'](?:${pattern})["']`, "m");
108
+ return (code) => code.includes("import") && code.includes("from") && importFromPattern.test(code);
109
+ }
103
110
  /**
104
111
  * Resolve a package.json exports value to a string entry path.
105
112
  * Prefers node → import → module → default conditions, recursing into nested objects.
@@ -417,14 +424,14 @@ function createOptimizeImportsPlugin(getNextConfig, getRoot) {
417
424
  };
418
425
  const entryPathCache = /* @__PURE__ */ new Map();
419
426
  let optimizedPackages = /* @__PURE__ */ new Set();
420
- let quotedPackages = [];
427
+ let hasOptimizedImportSource = () => false;
421
428
  const registeredBarrels = /* @__PURE__ */ new Set();
422
429
  return {
423
430
  name: "vinext:optimize-imports",
424
431
  buildStart() {
425
432
  const nextConfig = getNextConfig();
426
433
  optimizedPackages = new Set([...DEFAULT_OPTIMIZE_PACKAGES, ...nextConfig?.optimizePackageImports ?? []]);
427
- quotedPackages = [...optimizedPackages].flatMap((pkg) => [`"${pkg}"`, `'${pkg}'`]);
434
+ hasOptimizedImportSource = createOptimizedImportSourceMatcher(optimizedPackages);
428
435
  entryPathCache.clear();
429
436
  barrelCaches.exportMapCache.clear();
430
437
  barrelCaches.subpkgOrigin.clear();
@@ -445,12 +452,7 @@ function createOptimizeImportsPlugin(getNextConfig, getRoot) {
445
452
  const preferReactServer = env?.name === "rsc";
446
453
  if (id.startsWith("\0")) return null;
447
454
  const packages = optimizedPackages;
448
- let hasBarrelImport = false;
449
- for (const quoted of quotedPackages) if (code.includes(quoted)) {
450
- hasBarrelImport = true;
451
- break;
452
- }
453
- if (!hasBarrelImport) return null;
455
+ if (!hasOptimizedImportSource(code)) return null;
454
456
  let ast;
455
457
  try {
456
458
  ast = parseAst(code);
@@ -576,4 +578,4 @@ function createOptimizeImportsPlugin(getNextConfig, getRoot) {
576
578
  };
577
579
  }
578
580
  //#endregion
579
- export { DEFAULT_OPTIMIZE_PACKAGES, buildBarrelExportMap, createOptimizeImportsPlugin };
581
+ export { DEFAULT_OPTIMIZE_PACKAGES, buildBarrelExportMap, createOptimizeImportsPlugin, createOptimizedImportSourceMatcher };
@@ -1,8 +1,11 @@
1
+ import { importExportWithCommonJsFallback } from "../utils/commonjs-loader.js";
1
2
  import { createRequire } from "node:module";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
- import { pathToFileURL } from "node:url";
5
5
  //#region src/plugins/postcss.ts
6
+ async function loadPluginExport(resolvedPath) {
7
+ return importExportWithCommonJsFallback(resolvedPath);
8
+ }
6
9
  /**
7
10
  * PostCSS config file names to search for, in priority order.
8
11
  * Matches the same search order as postcss-load-config / lilconfig.
@@ -65,8 +68,7 @@ async function resolvePostcssStringPluginsUncached(projectRoot) {
65
68
  if (configPath.endsWith(".postcssrc")) {
66
69
  if (fs.readFileSync(configPath, "utf-8").trim().startsWith("{")) return;
67
70
  }
68
- const mod = await import(pathToFileURL(configPath).href);
69
- config = mod.default ?? mod;
71
+ config = await importExportWithCommonJsFallback(configPath);
70
72
  } catch {
71
73
  return;
72
74
  }
@@ -76,14 +78,12 @@ async function resolvePostcssStringPluginsUncached(projectRoot) {
76
78
  try {
77
79
  return { plugins: await Promise.all(config.plugins.filter(Boolean).map(async (plugin) => {
78
80
  if (typeof plugin === "string") {
79
- const mod = await import(pathToFileURL(req.resolve(plugin)).href);
80
- const fn = mod.default ?? mod;
81
+ const fn = await loadPluginExport(req.resolve(plugin));
81
82
  return typeof fn === "function" ? fn() : fn;
82
83
  }
83
84
  if (Array.isArray(plugin) && typeof plugin[0] === "string") {
84
85
  const [name, options] = plugin;
85
- const mod = await import(pathToFileURL(req.resolve(name)).href);
86
- const fn = mod.default ?? mod;
86
+ const fn = await loadPluginExport(req.resolve(name));
87
87
  return typeof fn === "function" ? fn(options) : fn;
88
88
  }
89
89
  return plugin;
@@ -0,0 +1,14 @@
1
+ //#region src/plugins/typeof-window.d.ts
2
+ type WindowType = "object" | "undefined";
3
+ type EnvironmentLike = {
4
+ config: {
5
+ consumer: "client" | "server";
6
+ };
7
+ };
8
+ declare function getTypeofWindowReplacement(environment: EnvironmentLike): WindowType;
9
+ declare function replaceTypeofWindow(code: string, replacement: WindowType): {
10
+ code: string;
11
+ map: import("magic-string").SourceMap;
12
+ } | null;
13
+ //#endregion
14
+ export { getTypeofWindowReplacement, replaceTypeofWindow };
@@ -0,0 +1,150 @@
1
+ import { collectBindingNames, forEachAstChild, hasRange, isAstRecord, isIdentifierNamed, nodeArray } from "./ast-utils.js";
2
+ import { parseAst } from "vite";
3
+ import MagicString from "magic-string";
4
+ //#region src/plugins/typeof-window.ts
5
+ function createScope(parent) {
6
+ return {
7
+ parent,
8
+ bindings: /* @__PURE__ */ new Set()
9
+ };
10
+ }
11
+ function hasBinding(scope, name) {
12
+ for (let current = scope; current; current = current.parent) if (current.bindings.has(name)) return true;
13
+ return false;
14
+ }
15
+ function collectScopeBindings(node, scope) {
16
+ forEachAstChild(node, (child) => {
17
+ if (child.type === "ExportNamedDeclaration" || child.type === "ExportDefaultDeclaration") {
18
+ if (isAstRecord(child.declaration)) collectScopeBindings(child, scope);
19
+ return;
20
+ }
21
+ if (child.type === "FunctionDeclaration" || child.type === "ClassDeclaration") {
22
+ collectBindingNames(child.id, scope.bindings);
23
+ return;
24
+ }
25
+ if (child.type === "VariableDeclaration") {
26
+ for (const declaration of nodeArray(child.declarations)) if (isAstRecord(declaration)) collectBindingNames(declaration.id, scope.bindings);
27
+ return;
28
+ }
29
+ if (child.type === "ImportDeclaration") {
30
+ for (const specifier of nodeArray(child.specifiers)) if (isAstRecord(specifier)) collectBindingNames(specifier.local, scope.bindings);
31
+ }
32
+ });
33
+ }
34
+ function collectVarBindings(node, scope, isRoot = true) {
35
+ if (!isRoot && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) return;
36
+ if (node.type === "VariableDeclaration" && node.kind === "var") {
37
+ for (const declaration of nodeArray(node.declarations)) if (isAstRecord(declaration)) collectBindingNames(declaration.id, scope.bindings);
38
+ }
39
+ forEachAstChild(node, (child) => collectVarBindings(child, scope, false));
40
+ }
41
+ function isFunctionNode(node) {
42
+ return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
43
+ }
44
+ function createChildScope(node, parent) {
45
+ if (node.type !== "Program" && node.type !== "BlockStatement" && node.type !== "StaticBlock" && node.type !== "SwitchStatement" && node.type !== "CatchClause" && node.type !== "ForStatement" && node.type !== "ForInStatement" && node.type !== "ForOfStatement" && node.type !== "ClassDeclaration" && node.type !== "ClassExpression") return null;
46
+ const scope = createScope(parent);
47
+ if (node.type === "ClassDeclaration" || node.type === "ClassExpression") collectBindingNames(node.id, scope.bindings);
48
+ else if (node.type === "CatchClause") collectBindingNames(node.param, scope.bindings);
49
+ collectScopeBindings(node, scope);
50
+ if (node.type === "SwitchStatement") {
51
+ for (const switchCase of nodeArray(node.cases)) if (isAstRecord(switchCase)) collectScopeBindings(switchCase, scope);
52
+ }
53
+ return scope;
54
+ }
55
+ function getTypeofWindowReplacement(environment) {
56
+ return environment.config.consumer === "client" ? "object" : "undefined";
57
+ }
58
+ function stringLiteralValue(node) {
59
+ if (!isAstRecord(node)) return null;
60
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
61
+ return null;
62
+ }
63
+ function evaluateTypeofWindowComparison(node, replacement, scope) {
64
+ if (!isAstRecord(node) || node.type !== "BinaryExpression") return null;
65
+ if (![
66
+ "==",
67
+ "===",
68
+ "!=",
69
+ "!=="
70
+ ].includes(String(node.operator))) return null;
71
+ const left = isAstRecord(node.left) ? node.left : null;
72
+ const right = isAstRecord(node.right) ? node.right : null;
73
+ const leftIsTypeofWindow = left?.type === "UnaryExpression" && left.operator === "typeof" && isIdentifierNamed(left.argument, "window") && !hasBinding(scope, "window");
74
+ const rightIsTypeofWindow = right?.type === "UnaryExpression" && right.operator === "typeof" && isIdentifierNamed(right.argument, "window") && !hasBinding(scope, "window");
75
+ const comparedValue = leftIsTypeofWindow ? stringLiteralValue(right) : rightIsTypeofWindow ? stringLiteralValue(left) : null;
76
+ if (comparedValue === null) return null;
77
+ const equal = replacement === comparedValue;
78
+ return node.operator === "==" || node.operator === "===" ? equal : !equal;
79
+ }
80
+ function replaceTypeofWindow(code, replacement) {
81
+ if (!/typeof\s+window/.test(code)) return null;
82
+ let ast;
83
+ try {
84
+ ast = parseAst(code);
85
+ } catch {
86
+ return null;
87
+ }
88
+ const output = new MagicString(code);
89
+ let changed = false;
90
+ if (!isAstRecord(ast)) return null;
91
+ const rootScope = createScope(null);
92
+ collectScopeBindings(ast, rootScope);
93
+ collectVarBindings(ast, rootScope);
94
+ function visit(node, parentScope) {
95
+ if (isFunctionNode(node)) {
96
+ const parameterScope = createScope(parentScope);
97
+ collectBindingNames(node.id, parameterScope.bindings);
98
+ for (const parameter of nodeArray(node.params)) {
99
+ collectBindingNames(parameter, parameterScope.bindings);
100
+ if (isAstRecord(parameter)) visit(parameter, parameterScope);
101
+ }
102
+ if (isAstRecord(node.body)) if (node.body.type === "BlockStatement") {
103
+ const bodyScope = createScope(parameterScope);
104
+ collectVarBindings(node.body, bodyScope);
105
+ visit(node.body, bodyScope);
106
+ } else visit(node.body, parameterScope);
107
+ return;
108
+ }
109
+ const scope = createChildScope(node, parentScope) ?? parentScope;
110
+ if (node.type === "IfStatement" && hasRange(node)) {
111
+ const result = evaluateTypeofWindowComparison(node.test, replacement, scope);
112
+ if (result !== null) {
113
+ const selected = result ? node.consequent : node.alternate;
114
+ if (isAstRecord(selected) && hasRange(selected)) {
115
+ output.remove(node.start, selected.start);
116
+ output.remove(selected.end, node.end);
117
+ visit(selected, scope);
118
+ } else output.overwrite(node.start, node.end, ";");
119
+ changed = true;
120
+ return;
121
+ }
122
+ }
123
+ if (node.type === "ConditionalExpression" && hasRange(node)) {
124
+ const result = evaluateTypeofWindowComparison(node.test, replacement, scope);
125
+ const selected = result ? node.consequent : node.alternate;
126
+ if (result !== null && isAstRecord(selected) && hasRange(selected)) {
127
+ output.overwrite(node.start, selected.start, "(");
128
+ if (selected.end < node.end) output.overwrite(selected.end, node.end, ")");
129
+ else output.appendLeft(selected.end, ")");
130
+ visit(selected, scope);
131
+ changed = true;
132
+ return;
133
+ }
134
+ }
135
+ if (node.type === "UnaryExpression" && node.operator === "typeof" && isIdentifierNamed(node.argument, "window") && !hasBinding(scope, "window") && hasRange(node)) {
136
+ output.overwrite(node.start, node.end, JSON.stringify(replacement));
137
+ changed = true;
138
+ return;
139
+ }
140
+ forEachAstChild(node, (child) => visit(child, scope));
141
+ }
142
+ for (const node of ast.body) if (isAstRecord(node)) visit(node, rootScope);
143
+ if (!changed) return null;
144
+ return {
145
+ code: output.toString(),
146
+ map: output.generateMap({ hires: "boundary" })
147
+ };
148
+ }
149
+ //#endregion
150
+ export { getTypeofWindowReplacement, replaceTypeofWindow };
@@ -21,7 +21,8 @@ type InterceptingRoute = {
21
21
  * @see https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/interception-routes.ts
22
22
  */
23
23
  sourceMatchPattern: string; /** Absolute path to the intercepting page component */
24
- pagePath: string; /** Absolute layout paths inside the intercepting route tree, outermost to innermost */
24
+ pagePath: string; /** Filesystem segments from app/ root to the intercepting page directory. */
25
+ sourcePageSegments?: string[]; /** Absolute layout paths inside the intercepting route tree, outermost to innermost */
25
26
  layoutPaths: string[]; /** Parameter names for dynamic segments */
26
27
  params: string[];
27
28
  /**
@@ -1,5 +1,5 @@
1
1
  import { normalizePathSeparators } from "../utils/path.js";
2
- import { compareRoutes, decodeRouteSegment, isInvisibleSegment } from "./utils.js";
2
+ import { decodeRouteSegment, isInvisibleSegment, sortRoutes } from "./utils.js";
3
3
  import { findFileWithExts, scanWithExtensions } from "./file-matcher.js";
4
4
  import { validateRoutePatterns } from "./route-validation.js";
5
5
  import { compareStrings } from "../utils/compare.js";
@@ -346,23 +346,25 @@ function createRouteManifestGraphVersion(segmentGraph) {
346
346
  }
347
347
  async function buildAppRouteGraph(appDir, matcher) {
348
348
  const routes = [];
349
+ const scanMatcher = { ...matcher };
350
+ findFileProbeCache.set(scanMatcher, /* @__PURE__ */ new Map());
349
351
  const excludeDir = (name) => name.startsWith("@") && name !== "@children" || name.startsWith("_") || isInterceptionMarkerDir(name);
350
- for await (const file of scanWithExtensions("**/page", appDir, matcher.extensions, excludeDir)) {
351
- const route = fileToAppRoute(file, appDir, "page", matcher);
352
+ for await (const file of scanWithExtensions("**/page", appDir, scanMatcher.extensions, excludeDir)) {
353
+ const route = fileToAppRoute(file, appDir, "page", scanMatcher);
352
354
  if (route) routes.push(route);
353
355
  }
354
- for await (const file of scanWithExtensions("**/route", appDir, matcher.extensions, excludeDir)) {
355
- const route = fileToAppRoute(file, appDir, "route", matcher);
356
+ for await (const file of scanWithExtensions("**/route", appDir, scanMatcher.extensions, excludeDir)) {
357
+ const route = fileToAppRoute(file, appDir, "route", scanMatcher);
356
358
  if (route) routes.push(route);
357
359
  }
358
360
  const routePatterns = new Set(routes.map((route) => route.pattern));
359
361
  const ghostParentRoutes = [];
360
- for await (const file of scanWithExtensions("**/layout", appDir, matcher.extensions, excludeDir)) {
362
+ for await (const file of scanWithExtensions("**/layout", appDir, scanMatcher.extensions, excludeDir)) {
361
363
  const dir = path.dirname(file);
362
364
  const routeDir = dir === "." ? appDir : path.join(appDir, dir);
363
365
  if (!hasParallelSlotDirectory(routeDir)) continue;
364
- if (discoverParallelSlots(routeDir, appDir, matcher).length === 0) continue;
365
- const route = directoryToAppRoute(dir, appDir, matcher, null, null);
366
+ if (discoverParallelSlots(routeDir, appDir, scanMatcher).length === 0) continue;
367
+ const route = directoryToAppRoute(dir, appDir, scanMatcher, null, null);
366
368
  if (!route) continue;
367
369
  if (routePatterns.has(route.pattern)) {
368
370
  ghostParentRoutes.push(route);
@@ -371,13 +373,13 @@ async function buildAppRouteGraph(appDir, matcher) {
371
373
  routes.push(route);
372
374
  routePatterns.add(route.pattern);
373
375
  }
374
- const slotSubRoutes = discoverSlotSubRoutes(routes, matcher, ghostParentRoutes);
376
+ const slotSubRoutes = discoverSlotSubRoutes(routes, scanMatcher, ghostParentRoutes);
375
377
  routes.push(...slotSubRoutes);
376
- discoverSiblingInterceptingRoutes(routes, appDir, matcher);
378
+ discoverSiblingInterceptingRoutes(routes, appDir, scanMatcher);
377
379
  validatePageRouteConflicts(routes, appDir);
378
380
  validateRoutePatterns(routes.map((route) => route.pattern));
379
381
  validateRoutePatterns([...new Set(routes.flatMap((route) => [...route.parallelSlots.flatMap((slot) => slot.interceptingRoutes.map((intercept) => intercept.targetPattern)), ...route.siblingIntercepts.map((intercept) => intercept.targetPattern)]))]);
380
- routes.sort(compareRoutes);
382
+ sortRoutes(routes);
381
383
  return {
382
384
  routes,
383
385
  routeManifest: createRouteManifest(routes)
@@ -1275,6 +1277,7 @@ function collectInterceptingPages(currentDir, interceptRoot, convention, interce
1275
1277
  targetPattern: targetPattern.pattern,
1276
1278
  sourceMatchPattern,
1277
1279
  pagePath: page,
1280
+ sourcePageSegments: normalizePathSeparators(path.relative(appDir, path.dirname(page))).split("/").filter(Boolean),
1278
1281
  params: targetPattern.params
1279
1282
  });
1280
1283
  }
@@ -1384,10 +1387,37 @@ function markerForInterceptionConvention(convention) {
1384
1387
  }
1385
1388
  }
1386
1389
  /**
1387
- * Find a file by name (without extension) in a directory.
1388
- * Checks configured pageExtensions.
1390
+ * Scan-scoped cache of convention-file probes, keyed by the per-scan matcher
1391
+ * created in `buildAppRouteGraph`. A single scan walks the appDir→leaf chain
1392
+ * separately for every route (layouts, templates, errors, boundaries, slots),
1393
+ * so shared ancestor directories — the `app/` root above all — get re-probed
1394
+ * once per descendant route. The probe result is deterministic within one scan
1395
+ * (the filesystem does not change mid-build), so memoizing it removes the
1396
+ * dominant cross-route redundancy.
1397
+ *
1398
+ * Keyed by matcher so the cache lifetime is exactly one `buildAppRouteGraph`
1399
+ * call: the scan registers a fresh matcher clone, and the entry is unreachable
1400
+ * (and GC-eligible) once the scan returns. A fresh key per scan is also what
1401
+ * makes this concurrency-safe — overlapping builds never share probe state.
1402
+ */
1403
+ const findFileProbeCache = /* @__PURE__ */ new WeakMap();
1404
+ /**
1405
+ * Find a file by name (without extension) in a directory, checking configured
1406
+ * pageExtensions. Memoizes through `findFileProbeCache` when the matcher has a
1407
+ * registered per-scan cache; otherwise falls back to a direct probe (identical
1408
+ * result). The `null` "not found" outcome is cached too, so repeated misses on
1409
+ * shared ancestors cost a single set of `existsSync` calls per scan.
1389
1410
  */
1390
- const findFile = findFileWithExts;
1411
+ function findFile(dir, name, matcher) {
1412
+ const cache = findFileProbeCache.get(matcher);
1413
+ if (!cache) return findFileWithExts(dir, name, matcher);
1414
+ const key = `${dir}\0${name}`;
1415
+ const cached = cache.get(key);
1416
+ if (cached !== void 0) return cached;
1417
+ const result = findFileWithExts(dir, name, matcher);
1418
+ cache.set(key, result);
1419
+ return result;
1420
+ }
1391
1421
  /**
1392
1422
  * Convert filesystem path segments to URL route parts, skipping invisible segments
1393
1423
  * (route groups, @slots, ".") and converting dynamic segment syntax to Express-style
@@ -43,9 +43,18 @@ declare function findFileWithExts(dir: string, name: string, matcher: ValidFileM
43
43
  * See: cloudflare/vinext#1502
44
44
  */
45
45
  declare function buildViteResolveExtensions(pageExtensions?: readonly string[] | null, viteDefaults?: readonly string[]): string[];
46
+ /**
47
+ * Normalize an explicit Next.js resolver extension list for Vite.
48
+ *
49
+ * Unlike `pageExtensions`, both Turbopack's `resolveExtensions` and webpack's
50
+ * `resolve.extensions` replace their resolver defaults. The empty string is a
51
+ * webpack/Turbopack convention for trying the import exactly as written; Vite
52
+ * already does that before appending extensions, so it must be omitted here.
53
+ */
54
+ declare function normalizeViteResolveExtensions(extensions: readonly string[]): string[];
46
55
  /**
47
56
  * Use function-form exclude for Node < 22.14 compatibility.
48
57
  */
49
58
  declare function scanWithExtensions(stem: string, cwd: string, extensions: readonly string[], exclude?: (name: string) => boolean): AsyncGenerator<string>;
50
59
  //#endregion
51
- export { ValidFileMatcher, buildViteResolveExtensions, createValidFileMatcher, findFileWithExtensions, findFileWithExts, normalizePageExtensions, scanWithExtensions };
60
+ export { ValidFileMatcher, buildViteResolveExtensions, createValidFileMatcher, findFileWithExtensions, findFileWithExts, normalizePageExtensions, normalizeViteResolveExtensions, scanWithExtensions };
@@ -113,6 +113,27 @@ function buildViteResolveExtensions(pageExtensions, viteDefaults = [
113
113
  return result;
114
114
  }
115
115
  /**
116
+ * Normalize an explicit Next.js resolver extension list for Vite.
117
+ *
118
+ * Unlike `pageExtensions`, both Turbopack's `resolveExtensions` and webpack's
119
+ * `resolve.extensions` replace their resolver defaults. The empty string is a
120
+ * webpack/Turbopack convention for trying the import exactly as written; Vite
121
+ * already does that before appending extensions, so it must be omitted here.
122
+ */
123
+ function normalizeViteResolveExtensions(extensions) {
124
+ const seen = /* @__PURE__ */ new Set();
125
+ const result = [];
126
+ for (const extension of extensions) {
127
+ const trimmed = extension.trim();
128
+ if (!trimmed) continue;
129
+ const dotted = trimmed.startsWith(".") ? trimmed : `.${trimmed}`;
130
+ if (seen.has(dotted)) continue;
131
+ seen.add(dotted);
132
+ result.push(dotted);
133
+ }
134
+ return result;
135
+ }
136
+ /**
116
137
  * Use function-form exclude for Node < 22.14 compatibility.
117
138
  */
118
139
  async function* scanWithExtensions(stem, cwd, extensions, exclude) {
@@ -123,4 +144,4 @@ async function* scanWithExtensions(stem, cwd, extensions, exclude) {
123
144
  })) yield file;
124
145
  }
125
146
  //#endregion
126
- export { buildViteResolveExtensions, createValidFileMatcher, findFileWithExtensions, findFileWithExts, normalizePageExtensions, scanWithExtensions };
147
+ export { buildViteResolveExtensions, createValidFileMatcher, findFileWithExtensions, findFileWithExts, normalizePageExtensions, normalizeViteResolveExtensions, scanWithExtensions };
@@ -1,4 +1,4 @@
1
- import { compareRoutes, decodeRouteSegment } from "./utils.js";
1
+ import { decodeRouteSegment, sortRoutes } from "./utils.js";
2
2
  import { createValidFileMatcher, scanWithExtensions } from "./file-matcher.js";
3
3
  import { patternToNextFormat, validateRoutePatterns } from "./route-validation.js";
4
4
  import { createRouteTrieCache, matchRouteWithTrie } from "./route-matching.js";
@@ -54,7 +54,7 @@ async function scanPageRoutes(pagesDir, matcher) {
54
54
  if (route) routes.push(route);
55
55
  }
56
56
  validateRoutePatterns(routes.map((route) => route.pattern));
57
- routes.sort(compareRoutes);
57
+ sortRoutes(routes);
58
58
  return routes;
59
59
  }
60
60
  /**
@@ -156,7 +156,7 @@ async function scanApiRoutes(pagesDir, matcher) {
156
156
  if (route) routes.push(route);
157
157
  }
158
158
  validateRoutePatterns(routes.map((route) => route.pattern));
159
- routes.sort(compareRoutes);
159
+ sortRoutes(routes);
160
160
  return routes;
161
161
  }
162
162
  //#endregion