vinext 1.0.1 → 1.1.0

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 (159) hide show
  1. package/README.md +72 -1
  2. package/dist/build/app-page-static-eligibility.d.ts +15 -0
  3. package/dist/build/app-page-static-eligibility.js +82 -0
  4. package/dist/build/prerender-paths.js +3 -78
  5. package/dist/build/prerender.js +33 -8
  6. package/dist/build/server-manifest.d.ts +7 -0
  7. package/dist/build/server-manifest.js +11 -1
  8. package/dist/cache/cache-adapters-virtual.d.ts +7 -1
  9. package/dist/cache/cache-adapters-virtual.js +3 -3
  10. package/dist/client/navigation-runtime.d.ts +11 -0
  11. package/dist/client/navigation-runtime.js +1 -1
  12. package/dist/config/config-matchers.js +3 -1
  13. package/dist/config/next-config.d.ts +7 -0
  14. package/dist/config/next-config.js +18 -3
  15. package/dist/entries/app-rsc-entry.d.ts +2 -0
  16. package/dist/entries/app-rsc-entry.js +64 -43
  17. package/dist/index.d.ts +4 -1
  18. package/dist/index.js +59 -30
  19. package/dist/plugins/cache-flight-codec.d.ts +19 -0
  20. package/dist/plugins/cache-flight-codec.js +68 -0
  21. package/dist/plugins/commonjs-esm-facade.d.ts +35 -0
  22. package/dist/plugins/commonjs-esm-facade.js +80 -0
  23. package/dist/plugins/fonts.d.ts +8 -11
  24. package/dist/plugins/fonts.js +71 -34
  25. package/dist/routing/file-matcher.d.ts +1 -0
  26. package/dist/routing/file-matcher.js +1 -1
  27. package/dist/server/api-handler.js +17 -7
  28. package/dist/server/app-browser-entry.js +11 -7
  29. package/dist/server/app-browser-history-controller.d.ts +2 -0
  30. package/dist/server/app-browser-history-controller.js +8 -0
  31. package/dist/server/app-browser-interception-context.js +26 -2
  32. package/dist/server/app-browser-navigation-controller.d.ts +7 -1
  33. package/dist/server/app-browser-navigation-controller.js +93 -10
  34. package/dist/server/app-fallback-renderer.d.ts +2 -0
  35. package/dist/server/app-fallback-renderer.js +1 -0
  36. package/dist/server/app-history-state.d.ts +3 -0
  37. package/dist/server/app-history-state.js +7 -0
  38. package/dist/server/app-middleware.js +2 -1
  39. package/dist/server/app-optimistic-routing.js +1 -1
  40. package/dist/server/app-page-boundary-render.d.ts +5 -0
  41. package/dist/server/app-page-boundary-render.js +6 -4
  42. package/dist/server/app-page-cache-finalizer.d.ts +11 -0
  43. package/dist/server/app-page-cache-finalizer.js +41 -7
  44. package/dist/server/app-page-cache-render.d.ts +18 -0
  45. package/dist/server/app-page-cache-render.js +63 -43
  46. package/dist/server/app-page-cache.d.ts +5 -0
  47. package/dist/server/app-page-cache.js +27 -7
  48. package/dist/server/app-page-dispatch.d.ts +9 -1
  49. package/dist/server/app-page-dispatch.js +47 -30
  50. package/dist/server/app-page-element-builder.d.ts +7 -0
  51. package/dist/server/app-page-element-builder.js +27 -42
  52. package/dist/server/app-page-execution.d.ts +15 -12
  53. package/dist/server/app-page-execution.js +30 -40
  54. package/dist/server/app-page-head.d.ts +5 -0
  55. package/dist/server/app-page-head.js +9 -3
  56. package/dist/server/app-page-probe.d.ts +0 -109
  57. package/dist/server/app-page-probe.js +3 -136
  58. package/dist/server/app-page-render-identity.d.ts +0 -1
  59. package/dist/server/app-page-render-identity.js +1 -1
  60. package/dist/server/app-page-render-observation.js +2 -2
  61. package/dist/server/app-page-render.d.ts +13 -5
  62. package/dist/server/app-page-render.js +219 -119
  63. package/dist/server/app-page-request.d.ts +0 -1
  64. package/dist/server/app-page-request.js +1 -2
  65. package/dist/server/app-page-response.d.ts +4 -3
  66. package/dist/server/app-page-response.js +4 -3
  67. package/dist/server/app-page-route-wiring.d.ts +17 -5
  68. package/dist/server/app-page-route-wiring.js +55 -33
  69. package/dist/server/app-page-rsc-render-status.d.ts +48 -0
  70. package/dist/server/app-page-rsc-render-status.js +54 -0
  71. package/dist/server/app-page-stream.d.ts +15 -8
  72. package/dist/server/app-page-stream.js +6 -0
  73. package/dist/server/app-request-stage-dispatch.js +1 -1
  74. package/dist/server/app-request-stage-independent-entry.js +4 -4
  75. package/dist/server/app-response-header-provenance.d.ts +2 -0
  76. package/dist/server/app-response-header-provenance.js +10 -4
  77. package/dist/server/app-route-handler-dispatch.js +1 -1
  78. package/dist/server/app-router-entry.js +3 -3
  79. package/dist/server/app-rsc-handler.d.ts +4 -0
  80. package/dist/server/app-rsc-handler.js +212 -96
  81. package/dist/server/app-rsc-request-normalization.d.ts +3 -1
  82. package/dist/server/app-rsc-request-normalization.js +4 -2
  83. package/dist/server/app-rsc-response-stage.d.ts +1 -1
  84. package/dist/server/app-rsc-response-stage.js +6 -4
  85. package/dist/server/app-rsc-route-matching.d.ts +9 -1
  86. package/dist/server/app-rsc-route-matching.js +36 -19
  87. package/dist/server/app-server-action-execution.js +1 -1
  88. package/dist/server/app-ssr-entry.d.ts +3 -2
  89. package/dist/server/app-ssr-entry.js +6 -4
  90. package/dist/server/app-ssr-stream.d.ts +6 -1
  91. package/dist/server/app-ssr-stream.js +34 -15
  92. package/dist/server/app-worker-stages.d.ts +2 -2
  93. package/dist/server/app-worker-stages.js +3 -3
  94. package/dist/server/cacheability-request.js +9 -5
  95. package/dist/server/config-headers.js +2 -0
  96. package/dist/server/dev-server.js +12 -2
  97. package/dist/server/headers.d.ts +14 -0
  98. package/dist/server/headers.js +15 -1
  99. package/dist/server/image-optimization.d.ts +16 -0
  100. package/dist/server/image-optimization.js +26 -6
  101. package/dist/server/isr-cache.js +2 -2
  102. package/dist/server/middleware-matcher-pattern.js +8 -4
  103. package/dist/server/middleware-response-headers.js +2 -0
  104. package/dist/server/multi-stage.d.ts +8 -0
  105. package/dist/server/navigation-planner.js +1 -1
  106. package/dist/server/node-response-signal.d.ts +16 -0
  107. package/dist/server/node-response-signal.js +26 -0
  108. package/dist/server/open-redirect.d.ts +47 -0
  109. package/dist/server/open-redirect.js +74 -1
  110. package/dist/server/pages-page-handler.js +21 -4
  111. package/dist/server/pages-request-stage-entry.js +8 -4
  112. package/dist/server/prod-server.d.ts +8 -7
  113. package/dist/server/prod-server.js +153 -69
  114. package/dist/server/request-pipeline.d.ts +20 -8
  115. package/dist/server/request-pipeline.js +21 -7
  116. package/dist/server/response-compression.d.ts +43 -0
  117. package/dist/server/response-compression.js +111 -0
  118. package/dist/server/seed-cache.js +10 -7
  119. package/dist/server/socket-error-backstop.d.ts +14 -0
  120. package/dist/server/socket-error-backstop.js +27 -4
  121. package/dist/shims/cache-adapter-instantiate.d.ts +50 -0
  122. package/dist/shims/cache-adapter-instantiate.js +118 -0
  123. package/dist/shims/cache-callable-runtime.d.ts +2 -40
  124. package/dist/shims/cache-callable-runtime.js +50 -168
  125. package/dist/shims/cache-flight-arguments.d.ts +22 -0
  126. package/dist/shims/cache-flight-arguments.js +56 -0
  127. package/dist/shims/cache-request-state.d.ts +15 -0
  128. package/dist/shims/cache-request-state.js +31 -8
  129. package/dist/shims/cache-runtime.d.ts +6 -19
  130. package/dist/shims/cache-runtime.js +165 -225
  131. package/dist/shims/cache.d.ts +2 -2
  132. package/dist/shims/cache.js +5 -4
  133. package/dist/shims/cacheability-classification.d.ts +7 -0
  134. package/dist/shims/error-boundary-navigation.d.ts +7 -0
  135. package/dist/shims/error-boundary-navigation.js +19 -1
  136. package/dist/shims/error-boundary.d.ts +10 -10
  137. package/dist/shims/error-boundary.js +26 -7
  138. package/dist/shims/fetch-cache.d.ts +6 -0
  139. package/dist/shims/fetch-cache.js +19 -1
  140. package/dist/shims/form.js +1 -1
  141. package/dist/shims/headers.d.ts +13 -0
  142. package/dist/shims/headers.js +47 -4
  143. package/dist/shims/internal/app-page-props-cache-key.d.ts +5 -2
  144. package/dist/shims/internal/app-page-props-cache-key.js +24 -15
  145. package/dist/shims/internal/headers-state.d.ts +0 -1
  146. package/dist/shims/metadata.js +3 -3
  147. package/dist/shims/navigation-errors.d.ts +6 -0
  148. package/dist/shims/navigation-errors.js +11 -1
  149. package/dist/shims/navigation.d.ts +7 -0
  150. package/dist/shims/navigation.js +14 -4
  151. package/dist/shims/request-context.d.ts +9 -0
  152. package/dist/shims/thenable-params.d.ts +4 -0
  153. package/dist/shims/thenable-params.js +25 -3
  154. package/dist/shims/unified-request-context.js +1 -0
  155. package/dist/utils/mdx-scan.d.ts +5 -0
  156. package/dist/utils/mdx-scan.js +8 -1
  157. package/dist/utils/regex-safety.d.ts +10 -0
  158. package/dist/utils/regex-safety.js +70 -18
  159. package/package.json +3 -3
@@ -0,0 +1,80 @@
1
+ import { parseAst } from "vite";
2
+ import MagicString from "magic-string";
3
+ //#region src/plugins/commonjs-esm-facade.ts
4
+ const EXPORT_FACADE_START = "/* [vite-plugin-commonjs] export-statement-S */";
5
+ const EXPORT_FACADE_END = "/* [vite-plugin-commonjs] export-statement-E */";
6
+ const EXPORT_KEYWORD_RE = /\bexport\b(?!-)/;
7
+ /** Whether `code` has a top-level ESM export statement. */
8
+ function hasEsmExports(code) {
9
+ if (!EXPORT_KEYWORD_RE.test(code)) return false;
10
+ let ast;
11
+ try {
12
+ ast = parseAst(code);
13
+ } catch {
14
+ return false;
15
+ }
16
+ return ast.body.some((statement) => statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" || statement.type === "ExportAllDeclaration");
17
+ }
18
+ /**
19
+ * Removes the export facade vite-plugin-commonjs appended to `output` when the
20
+ * module it transformed is ESM. Returns `undefined` when there is nothing to
21
+ * remove.
22
+ *
23
+ * Next.js treats a module with ESM syntax as ESM: `require()` still works, but
24
+ * `module` / `exports` never become its exports. The plugin's analyzer is not
25
+ * scope-aware, so for bundled ESM that inlines CommonJS wrappers
26
+ * (`__commonJS((exports, module) => …)`) it exports the wrappers' assignments
27
+ * too, which duplicates the module's own exports. Its `require()` rewrite and
28
+ * its local `module` / `exports` polyfill stay, so free CommonJS assignments
29
+ * still run without becoming exports.
30
+ *
31
+ * Besides the facade, the plugin only prepends imports and its polyfill and
32
+ * rewrites `require()` calls, so the module's own top-level exports are still
33
+ * in `output`. Classifying the output rather than rereading the source keeps
34
+ * the decision tied to the exact code the plugin transformed.
35
+ *
36
+ * The facade is appended after the module's code (only dynamic-require
37
+ * runtimes, which have no source mappings, may follow it), so removing it
38
+ * leaves the plugin's source map valid.
39
+ */
40
+ function stripEsmCommonJsExportFacade(output) {
41
+ const facade = findEsmCommonJsExportFacade(output);
42
+ return facade && output.slice(0, facade[0]) + output.slice(facade[1]);
43
+ }
44
+ /** The `[start, end)` range of the facade {@link stripEsmCommonJsExportFacade} removes. */
45
+ function findEsmCommonJsExportFacade(output) {
46
+ const start = output.lastIndexOf(EXPORT_FACADE_START);
47
+ if (start === -1) return void 0;
48
+ const end = output.indexOf(EXPORT_FACADE_END, start);
49
+ if (end === -1) return void 0;
50
+ const facadeEnd = end + 47;
51
+ return hasEsmExports(output.slice(0, start) + output.slice(facadeEnd)) ? [start, facadeEnd] : void 0;
52
+ }
53
+ /**
54
+ * Applies {@link stripEsmCommonJsExportFacade} in the client dependency
55
+ * optimizer's Rolldown builds (scan and pre-bundle). vite-plugin-commonjs's
56
+ * pre-bundle plugin loads and converts files there without vinext's transform
57
+ * wrapper, so its output reaches this hook as the loaded code. That code is
58
+ * the optimizer's source, so the removal needs its own map for anything the
59
+ * plugin appended after the facade.
60
+ */
61
+ const commonJsEsmFacadeOptimizeDepsPlugin = {
62
+ name: "vinext:commonjs-esm-facade:optimize-deps",
63
+ transform: {
64
+ filter: { code: { include: EXPORT_FACADE_START } },
65
+ handler(code, id) {
66
+ const facade = findEsmCommonJsExportFacade(code);
67
+ if (!facade) return null;
68
+ const output = new MagicString(code).remove(facade[0], facade[1]);
69
+ return {
70
+ code: output.toString(),
71
+ map: output.generateMap({
72
+ source: id,
73
+ hires: "boundary"
74
+ })
75
+ };
76
+ }
77
+ }
78
+ };
79
+ //#endregion
80
+ export { commonJsEsmFacadeOptimizeDepsPlugin, stripEsmCommonJsExportFacade };
@@ -3,9 +3,10 @@ import { Plugin } from "vite";
3
3
  export declare const VIRTUAL_GOOGLE_FONTS = "virtual:vinext-google-fonts";
4
4
  export declare const RESOLVED_VIRTUAL_GOOGLE_FONTS: string;
5
5
  /**
6
- * Rewrite absolute filesystem paths in cached Google Fonts CSS so the
7
- * `@font-face { src: url(...) }` references point at the served URL the
8
- * plugin's `writeBundle` hook copies the font files to.
6
+ * Point the `@font-face { src: url(...) }` references of cached Google Fonts
7
+ * CSS at the served URL the plugin's `writeBundle` hook copies the font
8
+ * files to (and the dev middleware serves them from), by replacing
9
+ * `CACHED_FONT_DIR_TOKEN` with `/<assetsDir>/_vinext_fonts`.
9
10
  *
10
11
  * This is called once per transform, before the CSS string is embedded in
11
12
  * the bundle as `_vinext.font.selfHostedCSS`. Every downstream consumer reads
@@ -14,9 +15,9 @@ export declare const RESOLVED_VIRTUAL_GOOGLE_FONTS: string;
14
15
  * in `shims/font-google-base.ts`), and the HTTP `Link:` response header
15
16
  * (via `buildAppPageFontLinkHeader` in `server/app-page-execution.ts`).
16
17
  *
17
- * Without this rewrite, all three emit the dev-machine filesystem path
18
- * (e.g. `/home/user/project/.vinext/fonts/geist-<hash>/geist-<hash>.woff2`)
19
- * and any production request fetches `<origin>/home/user/...` → 404.
18
+ * The token is substituted with plain split/join: it is a fixed string
19
+ * vinext wrote itself, so nothing about the checkout's path (spaces, quotes,
20
+ * parentheses, regex metacharacters, Windows separators) is parsed here.
20
21
  *
21
22
  * `assetsDir` must match whatever Vite has resolved for
22
23
  * `build.assetsDir` on the client environment — otherwise the embedded
@@ -26,12 +27,8 @@ export declare const RESOLVED_VIRTUAL_GOOGLE_FONTS: string;
26
27
  * passes the resolved value through from plugin state. The default is
27
28
  * kept only so the exported helper can be driven directly from unit
28
29
  * tests without synthesizing a full plugin context.
29
- *
30
- * Uses split/join rather than regex because `cacheDir` is an absolute
31
- * filesystem path that may contain regex metacharacters on unusual
32
- * filesystems.
33
30
  */
34
- export declare function _rewriteCachedFontCssToServedUrls(css: string, cacheDir: string, assetsDir?: string): string;
31
+ export declare function _rewriteCachedFontCssToServedUrls(css: string, assetsDir?: string): string;
35
32
  /**
36
33
  * Safely parse a static JS object literal string into a plain object.
37
34
  * Uses Vite's parseAst (Rollup/acorn) so no code is ever evaluated.
@@ -43,15 +43,15 @@ const GOOGLE_FONT_UTILITY_EXPORTS = /* @__PURE__ */ new Set([
43
43
  /**
44
44
  * Served URL prefix for self-hosted Google Font files.
45
45
  *
46
- * `fetchAndCacheFont()` downloads .woff2 files into `<root>/.vinext/fonts/`
46
+ * `fetchAndCacheFont()` downloads font files into `<root>/.vinext/fonts/`
47
47
  * and writes an `@font-face` CSS snippet whose `src: url(...)` references
48
- * the files by absolute filesystem path — convenient for disk, unusable at
49
- * runtime because browsers resolve relative to the origin. Before the CSS
50
- * is embedded in the bundle as `_vinext.font.selfHostedCSS`, the filesystem
51
- * prefix is rewritten to this URL prefix by `_rewriteCachedFontCssToServedUrls()`,
52
- * and the matching `writeBundle` hook in `createGoogleFontsPlugin` copies
53
- * the font files into `<clientOutDir>/<assetsDir>/_vinext_fonts/` so the
54
- * rewritten URL actually resolves against the origin at request time.
48
+ * name each file relative to that cache directory, behind
49
+ * `CACHED_FONT_DIR_TOKEN`. Before the CSS is embedded in the bundle as
50
+ * `_vinext.font.selfHostedCSS`, `_rewriteCachedFontCssToServedUrls()`
51
+ * replaces the token with this URL prefix, and the matching `writeBundle`
52
+ * hook in `createGoogleFontsPlugin` copies the font files into
53
+ * `<clientOutDir>/<assetsDir>/_vinext_fonts/` so the URL actually resolves
54
+ * against the origin at request time.
55
55
  *
56
56
  * The leading `_` keeps the namespace distinct from Vite's content-hashed
57
57
  * asset names (which are emitted flat into `<assetsDir>/`) and from any
@@ -59,6 +59,25 @@ const GOOGLE_FONT_UTILITY_EXPORTS = /* @__PURE__ */ new Set([
59
59
  */
60
60
  const VINEXT_FONT_URL_NAMESPACE = "_vinext_fonts";
61
61
  const MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH = 500;
62
+ /**
63
+ * Stands in for the font cache directory in every `url()` of a cached
64
+ * stylesheet, so the file on disk holds no absolute path and reads the same
65
+ * wherever the checkout's `.vinext/` ends up: a moved or copied project, a
66
+ * CI cache restored into another workspace, or a Docker build that copies
67
+ * in a host's `.vinext/`. Next.js has no such cache (its Google font loader
68
+ * keeps fetched CSS in memory and emits `/_next/static/media` URLs), so the
69
+ * parity target is output that does not depend on where the cache was
70
+ * written.
71
+ */
72
+ const CACHED_FONT_DIR_TOKEN = "__VINEXT_FONT_CACHE_DIR__";
73
+ /**
74
+ * Cached stylesheet whose `url()` references use `CACHED_FONT_DIR_TOKEN`.
75
+ * The suffix ties the cache to that format: a `style.css` written by an
76
+ * earlier vinext holds the writing checkout's absolute paths, which would
77
+ * 404 once embedded, so it is never read and the font is fetched again.
78
+ * Change the suffix whenever the cached CSS format changes.
79
+ */
80
+ const CACHED_FONT_CSS_FILE = "style-q7f3kd.css";
62
81
  function formatGoogleFontsErrorBody(body) {
63
82
  const trimmed = body.trim();
64
83
  if (!trimmed) return "(empty response body)";
@@ -67,9 +86,10 @@ function formatGoogleFontsErrorBody(body) {
67
86
  return `${trimmed.slice(0, MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH)}\n... (truncated ${omitted} characters)`;
68
87
  }
69
88
  /**
70
- * Rewrite absolute filesystem paths in cached Google Fonts CSS so the
71
- * `@font-face { src: url(...) }` references point at the served URL the
72
- * plugin's `writeBundle` hook copies the font files to.
89
+ * Point the `@font-face { src: url(...) }` references of cached Google Fonts
90
+ * CSS at the served URL the plugin's `writeBundle` hook copies the font
91
+ * files to (and the dev middleware serves them from), by replacing
92
+ * `CACHED_FONT_DIR_TOKEN` with `/<assetsDir>/_vinext_fonts`.
73
93
  *
74
94
  * This is called once per transform, before the CSS string is embedded in
75
95
  * the bundle as `_vinext.font.selfHostedCSS`. Every downstream consumer reads
@@ -78,9 +98,9 @@ function formatGoogleFontsErrorBody(body) {
78
98
  * in `shims/font-google-base.ts`), and the HTTP `Link:` response header
79
99
  * (via `buildAppPageFontLinkHeader` in `server/app-page-execution.ts`).
80
100
  *
81
- * Without this rewrite, all three emit the dev-machine filesystem path
82
- * (e.g. `/home/user/project/.vinext/fonts/geist-<hash>/geist-<hash>.woff2`)
83
- * and any production request fetches `<origin>/home/user/...` → 404.
101
+ * The token is substituted with plain split/join: it is a fixed string
102
+ * vinext wrote itself, so nothing about the checkout's path (spaces, quotes,
103
+ * parentheses, regex metacharacters, Windows separators) is parsed here.
84
104
  *
85
105
  * `assetsDir` must match whatever Vite has resolved for
86
106
  * `build.assetsDir` on the client environment — otherwise the embedded
@@ -90,16 +110,9 @@ function formatGoogleFontsErrorBody(body) {
90
110
  * passes the resolved value through from plugin state. The default is
91
111
  * kept only so the exported helper can be driven directly from unit
92
112
  * tests without synthesizing a full plugin context.
93
- *
94
- * Uses split/join rather than regex because `cacheDir` is an absolute
95
- * filesystem path that may contain regex metacharacters on unusual
96
- * filesystems.
97
113
  */
98
- function _rewriteCachedFontCssToServedUrls(css, cacheDir, assetsDir = DEFAULT_ASSETS_DIR) {
99
- const normalizedCacheDir = toSlash(cacheDir);
100
- if (!normalizedCacheDir || !css.includes(normalizedCacheDir)) return css;
101
- const prefix = assetsDir || DEFAULT_ASSETS_DIR;
102
- return css.split(normalizedCacheDir).join(`/${prefix}/${VINEXT_FONT_URL_NAMESPACE}`);
114
+ function _rewriteCachedFontCssToServedUrls(css, assetsDir = DEFAULT_ASSETS_DIR) {
115
+ return css.split(CACHED_FONT_DIR_TOKEN).join(`/${assetsDir || DEFAULT_ASSETS_DIR}/${VINEXT_FONT_URL_NAMESPACE}`);
103
116
  }
104
117
  /**
105
118
  * Default `build.assetsDir` — matches vinext's resolved default in
@@ -274,24 +287,49 @@ function propertyNameToGoogleFontFamily(prop) {
274
287
  }
275
288
  /**
276
289
  * Fetch Google Fonts CSS, download .woff2 files, cache locally, and return
277
- * @font-face CSS with local file references.
290
+ * @font-face CSS whose `url()` references use `CACHED_FONT_DIR_TOKEN`.
278
291
  *
279
292
  * Cache dir structure: .vinext/fonts/<family-hash>/
280
- * - style.css (the rewritten @font-face CSS)
293
+ * - style-<suffix>.css (the rewritten @font-face CSS, see `CACHED_FONT_CSS_FILE`)
281
294
  * - *.woff2 (downloaded font files)
282
295
  */
283
296
  async function fetchAndCacheFont(cssUrl, family, cacheDir) {
284
297
  const { createHash } = await import("node:crypto");
285
298
  const urlHash = createHash("md5").update(cssUrl).digest("hex").slice(0, 12);
286
- const fontDir = path.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${urlHash}`);
287
- const cachedCSSPath = path.join(fontDir, "style.css");
299
+ const fontDirName = `${family.toLowerCase().replace(/\s+/g, "-")}-${urlHash}`;
300
+ const fontDir = path.join(cacheDir, fontDirName);
301
+ const cachedCSSPath = path.join(fontDir, CACHED_FONT_CSS_FILE);
288
302
  if (fs.existsSync(cachedCSSPath)) return fs.readFileSync(cachedCSSPath, "utf-8");
303
+ const css = await downloadGoogleFont(cssUrl, family, fontDir, fontDirName);
304
+ try {
305
+ writeFileAtomically(cachedCSSPath, css);
306
+ } catch {}
307
+ return css;
308
+ }
309
+ /**
310
+ * Write-then-rename, so a write that fails midway never leaves a partial
311
+ * file for a later build to trust: both font files and the stylesheet are
312
+ * reused as soon as they exist.
313
+ */
314
+ function writeFileAtomically(filePath, data) {
315
+ const tempPath = `${filePath}.${process.pid}.tmp`;
316
+ try {
317
+ fs.writeFileSync(tempPath, data);
318
+ fs.renameSync(tempPath, filePath);
319
+ } catch (err) {
320
+ fs.rmSync(tempPath, { force: true });
321
+ throw err;
322
+ }
323
+ }
324
+ async function downloadGoogleFont(cssUrl, family, fontDir, fontDirName) {
325
+ const { createHash } = await import("node:crypto");
289
326
  const cssResponse = await fetch(cssUrl, { headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" } });
290
327
  if (!cssResponse.ok) {
291
328
  const body = await cssResponse.text().catch(() => "");
292
329
  throw new GoogleFontsHttpError(cssUrl, cssResponse.status, body);
293
330
  }
294
331
  let css = await cssResponse.text();
332
+ if (!css.trim()) throw new Error(`Google Fonts returned an empty stylesheet: ${cssUrl}`);
295
333
  const urlRe = /url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)/g;
296
334
  const urls = /* @__PURE__ */ new Map();
297
335
  let urlMatch;
@@ -308,14 +346,13 @@ async function fetchAndCacheFont(cssUrl, family, cacheDir) {
308
346
  const filePath = path.join(fontDir, filename);
309
347
  if (!fs.existsSync(filePath)) {
310
348
  const fontResponse = await fetch(fontUrl);
311
- if (fontResponse.ok) {
312
- const buffer = Buffer.from(await fontResponse.arrayBuffer());
313
- fs.writeFileSync(filePath, buffer);
314
- }
349
+ if (!fontResponse.ok) throw new Error(`Font file download failed with HTTP ${fontResponse.status}: ${fontUrl}`);
350
+ const buffer = Buffer.from(await fontResponse.arrayBuffer());
351
+ if (buffer.byteLength === 0) throw new Error(`Font file download returned an empty body: ${fontUrl}`);
352
+ writeFileAtomically(filePath, buffer);
315
353
  }
316
- css = css.split(fontUrl).join(filePath);
354
+ css = css.split(fontUrl).join(`${CACHED_FONT_DIR_TOKEN}/${fontDirName}/${filename}`);
317
355
  }
318
- fs.writeFileSync(cachedCSSPath, css);
319
356
  return css;
320
357
  }
321
358
  /**
@@ -546,7 +583,7 @@ function createGoogleFontsPlugin(fontGoogleShimPath, shimsDir) {
546
583
  if (err instanceof GoogleFontsHttpError) throw new Error(`[vinext:google-fonts] ${id}: Google Fonts returned HTTP ${err.status} for ${err.url}.\n${formatGoogleFontsErrorBody(err.responseBody)}`);
547
584
  return;
548
585
  }
549
- const servedCSS = _rewriteCachedFontCssToServedUrls(localCSS, cacheDir, transformAssetsDir);
586
+ const servedCSS = _rewriteCachedFontCssToServedUrls(localCSS, transformAssetsDir);
550
587
  const preloadUrls = findFontFilesInCss(servedCSS, validated.preload ? validated.subsets : void 0).filter((file) => file.preloadFontFile).map((file) => file.googleFontFileUrl);
551
588
  const fallbackMetrics = validated.adjustFontFallback === false ? void 0 : getFallbackFontOverrideMetrics(family);
552
589
  const adjustedFallbackCSS = fallbackMetrics ? buildFallbackFontFace(family, fallbackMetrics) : void 0;
@@ -1,4 +1,5 @@
1
1
  //#region src/routing/file-matcher.d.ts
2
+ export declare const DEFAULT_PAGE_EXTENSIONS: readonly ["tsx", "ts", "jsx", "js"];
2
3
  export declare function normalizePageExtensions(pageExtensions?: readonly string[] | null): string[];
3
4
  export type ValidFileMatcher = {
4
5
  extensions: string[];
@@ -152,4 +152,4 @@ async function* scanWithExtensions(stem, cwd, extensions, exclude) {
152
152
  })) yield toSlash(file);
153
153
  }
154
154
  //#endregion
155
- export { buildViteResolveExtensions, createValidFileMatcher, findFileWithExtensions, findFileWithExts, normalizePageExtensions, normalizeViteResolveExtensions, scanWithExtensions };
155
+ export { DEFAULT_PAGE_EXTENSIONS, buildViteResolveExtensions, createValidFileMatcher, findFileWithExtensions, findFileWithExts, normalizePageExtensions, normalizeViteResolveExtensions, scanWithExtensions };
@@ -11,6 +11,7 @@ import { resolveRequestHost, resolveRequestProtocol } from "./proxy-trust.js";
11
11
  import { performOnDemandRevalidate } from "./pages-revalidate.js";
12
12
  import { attachPagesPreviewApi, attachPagesRequestCookies } from "./pages-node-compat.js";
13
13
  import { finalizeEdgeApiResponse, isEdgeApiRuntime } from "./edge-api-runtime.js";
14
+ import { signalFromNodeResponse } from "./node-response-signal.js";
14
15
  import { Buffer } from "node:buffer";
15
16
  import { decode } from "node:querystring";
16
17
  //#region src/server/api-handler.ts
@@ -89,7 +90,7 @@ function readEdgeRequestBody(req) {
89
90
  req.on("error", (error) => controller.error(error));
90
91
  } });
91
92
  }
92
- function createEdgeApiRequest(req, url, params, nextConfig) {
93
+ function createEdgeApiRequest(req, res, url, params, nextConfig) {
93
94
  const headers = new Headers();
94
95
  for (const [name, value] of Object.entries(req.headers)) {
95
96
  if (name.startsWith(":")) continue;
@@ -103,10 +104,12 @@ function createEdgeApiRequest(req, url, params, nextConfig) {
103
104
  if (basePath && !hasBasePath(requestUrl.pathname, basePath)) requestUrl.pathname = `${basePath}${requestUrl.pathname}`;
104
105
  const query = mergeRouteParamsIntoQuery(parseQueryString(url), params);
105
106
  requestUrl.search = urlQueryToSearchParams(query).toString();
106
- const body = readEdgeRequestBody(req);
107
+ const signal = signalFromNodeResponse(res);
108
+ const body = signal.aborted ? void 0 : readEdgeRequestBody(req);
107
109
  const init = {
108
110
  headers,
109
- method: req.method
111
+ method: req.method,
112
+ signal
110
113
  };
111
114
  if (body) {
112
115
  init.body = body;
@@ -138,12 +141,17 @@ function waitForWritableDrain(res) {
138
141
  res.once("close", onClose);
139
142
  });
140
143
  }
141
- async function writeEdgeApiResponseBody(res, body) {
144
+ async function writeEdgeApiResponseBody(res, body, signal) {
142
145
  if (!body) {
143
146
  res.end();
144
147
  return;
145
148
  }
146
149
  const reader = body.getReader();
150
+ const cancel = () => {
151
+ reader.cancel(signal.reason).catch(() => {});
152
+ };
153
+ if (signal.aborted) cancel();
154
+ else signal.addEventListener("abort", cancel, { once: true });
147
155
  try {
148
156
  while (true) {
149
157
  const result = await reader.read();
@@ -151,11 +159,13 @@ async function writeEdgeApiResponseBody(res, body) {
151
159
  if (result.value.byteLength === 0) continue;
152
160
  if (!res.write(Buffer.from(result.value))) await waitForWritableDrain(res);
153
161
  }
154
- res.end();
162
+ if (!signal.aborted) res.end();
155
163
  } catch (error) {
164
+ if (signal.aborted) return;
156
165
  res.destroy(error instanceof Error ? error : new Error(String(error)));
157
166
  throw error;
158
167
  } finally {
168
+ signal.removeEventListener("abort", cancel);
159
169
  reader.releaseLock();
160
170
  }
161
171
  }
@@ -224,7 +234,7 @@ async function handleApiRoute(runner, req, res, url, apiRoutes, nextConfig) {
224
234
  try {
225
235
  const apiModule = await importModule(runner, route.filePath);
226
236
  if (isEdgeApiRouteModule(apiModule)) {
227
- const nextRequest = new NextRequest(createEdgeApiRequest(req, url, params, nextConfig), nextConfig ? { nextConfig: {
237
+ const nextRequest = new NextRequest(createEdgeApiRequest(req, res, url, params, nextConfig), nextConfig ? { nextConfig: {
228
238
  basePath: nextConfig.basePath,
229
239
  i18n: nextConfig.i18n ?? void 0,
230
240
  trailingSlash: nextConfig.trailingSlash
@@ -239,7 +249,7 @@ async function handleApiRoute(runner, req, res, url, apiRoutes, nextConfig) {
239
249
  if (name !== "set-cookie") res.setHeader(name, value);
240
250
  });
241
251
  if (setCookieHeaders.length) res.setHeader("set-cookie", setCookieHeaders);
242
- await writeEdgeApiResponseBody(res, response.body);
252
+ await writeEdgeApiResponseBody(res, response.body, nextRequest.signal);
243
253
  return true;
244
254
  }
245
255
  const handler = apiModule.default;
@@ -1,12 +1,12 @@
1
1
  import { splitPathnameForRouteMatch } from "../routing/utils.js";
2
2
  import { addBasePathToPathname, stripBasePath } from "../utils/base-path.js";
3
3
  import { VINEXT_CLIENT_REUSE_MANIFEST_HEADER, VINEXT_PARAMS_HEADER, VINEXT_RENDERED_PATH_AND_SEARCH_HEADER, VINEXT_RSC_REDIRECT_HEADER, VINEXT_RSC_REDIRECT_TYPE_HEADER } from "./headers.js";
4
- import { AppElementsWire } from "./app-elements-wire.js";
5
- import { getMountedSlotIdsHeader, resolveVisitedResponseInterceptionContext } from "./app-elements.js";
6
4
  import { AppRouterContext } from "../shims/internal/app-router-context.js";
7
5
  import { matchRoutePattern } from "../routing/route-pattern.js";
8
6
  import { installWindowNext, setWindowNextInternalSourcePage } from "../client/window-next.js";
9
7
  import { retryScrollTo, scrollToHashTargetOnNextFrame } from "../shims/hash-scroll.js";
8
+ import { AppElementsWire } from "./app-elements-wire.js";
9
+ import { getMountedSlotIdsHeader, resolveVisitedResponseInterceptionContext } from "./app-elements.js";
10
10
  import { getBfcacheIdMapContext, getNavigationContext, setNavigationContext } from "../shims/navigation-context-state.js";
11
11
  import { decodeRedirectError, isRedirectError } from "../shims/navigation-errors.js";
12
12
  import DefaultGlobalError from "../shims/default-global-error.js";
@@ -362,22 +362,22 @@ function createActionInitiationSnapshot() {
362
362
  }
363
363
  function createNavigationCommitEffect(options) {
364
364
  const { activeRoutePaths, bfcacheIds, href, historyUpdateMode, navId, params, previousNextUrl, targetHistoryIndex } = options;
365
- return () => {
365
+ return ({ keepCurrentUrl, releaseSnapshot }) => {
366
366
  if (!browserNavigationController.isCurrentNavigation(navId)) {
367
- commitClientNavigationState(void 0, { releaseSnapshot: true });
367
+ commitClientNavigationState(void 0, { releaseSnapshot });
368
368
  return;
369
369
  }
370
370
  historyController.commitNavigationHistory({
371
371
  activeRoutePaths,
372
372
  bfcacheIds,
373
373
  href,
374
- historyUpdateMode,
374
+ historyUpdateMode: keepCurrentUrl ? void 0 : historyUpdateMode,
375
375
  previousNextUrl,
376
376
  stageClientParams: () => stageClientParams(params),
377
377
  targetHistoryIndex
378
378
  });
379
379
  clearAppNavigationFailureTarget(href);
380
- commitClientNavigationState(navId);
380
+ commitClientNavigationState(navId, { releaseSnapshot });
381
381
  };
382
382
  }
383
383
  async function renderNavigationPayload(options) {
@@ -1505,7 +1505,11 @@ function bootstrapHydration(rscStream, devErrorOverlay, initialRscBootstrap) {
1505
1505
  },
1506
1506
  preparePrefetchResponse: (response) => decodeAppElementsPromise(createFromFetch(Promise.resolve(response))),
1507
1507
  claimCurrentHistoryTreeSnapshot: (historyUpdateMode, previousHistoryState) => historyController.claimCurrentHistoryTreeSnapshot(historyUpdateMode, previousHistoryState),
1508
- commitAppOwnedHistoryStateWrite: (historyUpdateMode, previousHistoryState) => historyController.commitAppOwnedHistoryStateWrite(historyUpdateMode, previousHistoryState)
1508
+ commitAppOwnedHistoryStateWrite: (historyUpdateMode, previousHistoryState) => historyController.commitAppOwnedHistoryStateWrite(historyUpdateMode, previousHistoryState),
1509
+ discardPendingNavigation: () => {
1510
+ if (browserNavigationController.discardPendingNavigation(historyController.readCurrentTreeSnapshot())) navigationAbortCoordinator.abortActive();
1511
+ },
1512
+ flushCommittingNavigationUrl: () => browserNavigationController.flushCommittingNavigationUrl()
1509
1513
  });
1510
1514
  const handlePopstate = createPopstateRestoreHandler({
1511
1515
  getActiveNavigationId: browserNavigationController.getActiveNavigationId.bind(browserNavigationController),
@@ -79,6 +79,8 @@ export declare class AppBrowserHistoryController {
79
79
  beginCacheInvalidationGuard(): () => void;
80
80
  invalidateRestorableClientState(): void;
81
81
  rememberHistoryStateSnapshot(state: AppRouterState): void;
82
+ /** The router state of the most recently committed render, if any. */
83
+ readCurrentTreeSnapshot(): AppRouterState | null;
82
84
  isCurrentExternalHistoryTree(historyState: unknown): boolean;
83
85
  /** Records the raw/hash history entry that now claims the live tree. */
84
86
  claimCurrentHistoryTreeSnapshot(historyUpdateMode: HistoryUpdateMode, previousHistoryState: unknown): void;
@@ -96,6 +96,9 @@ var AppBrowserHistoryController = class {
96
96
  this.#treeSnapshotIdPendingFreshState = historyTreeSnapshotId !== null && this.#treeSnapshots.has(historyTreeSnapshotId) ? historyTreeSnapshotId : null;
97
97
  }
98
98
  rememberHistoryStateSnapshot(state) {
99
+ const previousState = this.readCurrentTreeSnapshot();
100
+ const previousOperation = previousState?.activeOperation;
101
+ if (previousState && previousOperation?.navigationCommitKind === "detached" && state.activeOperation?.navigationCommitKind === "authoritative" && state.activeOperation.navigationId === previousOperation.navigationId) this.#restorableClientState.replaceHistoryStateSnapshots(previousState, state);
99
102
  this.#restorableClientState.rememberHistoryStateSnapshot({
100
103
  historyIndex: this.#currentHistoryTraversalIndex,
101
104
  state
@@ -109,6 +112,11 @@ var AppBrowserHistoryController = class {
109
112
  if (previousTreeSnapshotId !== null && previousTreeSnapshotId !== treeSnapshotId && !this.#treeSnapshotClaimCounts.has(previousTreeSnapshotId)) this.#treeSnapshots.delete(previousTreeSnapshotId);
110
113
  if (historyTreeSnapshotId !== treeSnapshotId) this.#replaceHistoryState(createHistoryStateWithTreeSnapshotId(this.#readHistoryState(), treeSnapshotId));
111
114
  }
115
+ /** The router state of the most recently committed render, if any. */
116
+ readCurrentTreeSnapshot() {
117
+ if (this.#currentTreeSnapshotId === null) return null;
118
+ return this.#treeSnapshots.get(this.#currentTreeSnapshotId) ?? null;
119
+ }
112
120
  isCurrentExternalHistoryTree(historyState) {
113
121
  const treeSnapshotId = readHistoryStateTreeSnapshotId(historyState);
114
122
  return treeSnapshotId !== null && treeSnapshotId === this.#currentTreeSnapshotId;
@@ -1,5 +1,6 @@
1
- import { splitPathnameForRouteMatch } from "../routing/utils.js";
1
+ import { normalizePathnameForRouteMatch, splitPathnameForRouteMatch } from "../routing/utils.js";
2
2
  import { stripBasePath } from "../utils/base-path.js";
3
+ import { normalizePath } from "./normalize-path.js";
3
4
  import { matchRoutePattern, matchRoutePatternPrefix, matchRoutePatternWithOptionalDynamicSegments } from "../routing/route-pattern.js";
4
5
  //#region src/server/app-browser-interception-context.ts
5
6
  /**
@@ -37,9 +38,32 @@ function resolveMiddlewareRewriteNavigationInterceptionContext(options) {
37
38
  for (const interception of options.routeManifest.segmentGraph.interceptions.values()) {
38
39
  if (!matchRoutePatternWithOptionalDynamicSegments(targetParts, interception.targetPatternParts)) continue;
39
40
  if (matchRoutePatternPrefix(sourceParts, interception.sourcePatternParts)) return currentPathname;
40
- if (currentMatchedPathname !== null && matchedSourceParts !== null && matchRoutePatternPrefix(matchedSourceParts, interception.sourcePatternParts)) return currentMatchedPathname;
41
+ if (currentMatchedPathname !== null && matchedSourceParts !== null && matchRoutePatternPrefix(matchedSourceParts, interception.sourcePatternParts)) return encodeMatchedPathname(currentMatchedPathname);
41
42
  }
42
43
  return null;
43
44
  }
45
+ /**
46
+ * The matched route pathname is the matched URL decoded once per segment, with
47
+ * path delimiters (and literal `%2F`-style text) re-escaped. The server matches
48
+ * the context on its raw segments, so undo that: keep the escaped delimiters and
49
+ * encode every other `%`, then let the URL parser encode the rest (`café`
50
+ * becomes `caf%C3%A9`). An escaped ASCII character comes back unescaped (`%61`
51
+ * as `a`), like Next.js's `Next-Url`, which it builds from the router tree's
52
+ * canonical params; both spellings match the same params. `%252F` is
53
+ * ambiguous, since both a literal `%2F` and a literal `%252F` produce it. That
54
+ * case, and a result that does not reproduce the same matched pathname because
55
+ * the URL parser changed it (stripping a control character, say), navigate
56
+ * without interception instead of naming a different source.
57
+ */
58
+ function encodeMatchedPathname(pathname) {
59
+ if (/%25(?:2f|23|3f|5c)/i.test(pathname)) return null;
60
+ let encoded;
61
+ try {
62
+ encoded = new URL(pathname.replace(/%(?!(?:25)?(?:2f|23|3f|5c))/gi, "%25"), "http://n").pathname;
63
+ } catch {
64
+ return null;
65
+ }
66
+ return normalizePath(normalizePathnameForRouteMatch(encoded)) === pathname ? encoded : null;
67
+ }
44
68
  //#endregion
45
69
  export { resolveManifestNavigationInterceptionContext, resolveMiddlewareRewriteNavigationInterceptionContext };
@@ -17,7 +17,10 @@ export type PendingBrowserRouterState = {
17
17
  };
18
18
  export type NavigationPayloadOutcome = "committed" | "no-commit" | "hard-navigate";
19
19
  type HardNavigationMode = "assign" | "replace";
20
- type BrowserNavigationCommitEffect = () => void;
20
+ type BrowserNavigationCommitEffect = (options: {
21
+ keepCurrentUrl: boolean;
22
+ releaseSnapshot: boolean;
23
+ }) => void;
21
24
  type BrowserNavigationCommitEffectFactory = (options: {
22
25
  activeRoutePaths: readonly string[];
23
26
  bfcacheIds: Readonly<Record<string, string>>;
@@ -78,6 +81,8 @@ type BrowserNavigationController = {
78
81
  attachBrowserRouterState(setter: Dispatch<AppRouterState | Promise<AppRouterState>>, stateRef: BrowserRouterStateRef): () => void;
79
82
  beginPendingBrowserRouterState(): PendingBrowserRouterState;
80
83
  finalizeNavigation(navId: number, pending: PendingBrowserRouterState | null | undefined): void;
84
+ discardPendingNavigation(visibleState: AppRouterState | null): boolean;
85
+ flushCommittingNavigationUrl(): void;
81
86
  restoreHistorySnapshotVisibleState(options: {
82
87
  restoreCopiedExternalHistoryEntry?: boolean;
83
88
  beforeCommit?: () => void;
@@ -100,6 +105,7 @@ type BrowserNavigationController = {
100
105
  * navigation would otherwise be lost.
101
106
  */
102
107
  drainPrePaintEffects(renderId: number): void;
108
+ beginNavigationRenderCommit(renderId: number): void;
103
109
  commitNavigationRender(renderId: number): void;
104
110
  clearCommittedNavigationFailureTargets(renderId: number): void;
105
111
  NavigationCommitSignal(this: void, { renderId, children }: {