vinext 0.0.54 → 0.0.55

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 (135) hide show
  1. package/README.md +1 -0
  2. package/dist/check.js +15 -3
  3. package/dist/check.js.map +1 -1
  4. package/dist/client/navigation-runtime.d.ts +1 -0
  5. package/dist/client/navigation-runtime.js +1 -1
  6. package/dist/client/navigation-runtime.js.map +1 -1
  7. package/dist/config/next-config.d.ts +14 -1
  8. package/dist/config/next-config.js +24 -4
  9. package/dist/config/next-config.js.map +1 -1
  10. package/dist/config/tsconfig-paths.d.ts +12 -3
  11. package/dist/config/tsconfig-paths.js +55 -24
  12. package/dist/config/tsconfig-paths.js.map +1 -1
  13. package/dist/entries/app-rsc-entry.d.ts +2 -1
  14. package/dist/entries/app-rsc-entry.js +12 -0
  15. package/dist/entries/app-rsc-entry.js.map +1 -1
  16. package/dist/entries/app-rsc-manifest.js +22 -5
  17. package/dist/entries/app-rsc-manifest.js.map +1 -1
  18. package/dist/entries/pages-server-entry.js +41 -4
  19. package/dist/entries/pages-server-entry.js.map +1 -1
  20. package/dist/index.js +81 -39
  21. package/dist/index.js.map +1 -1
  22. package/dist/plugins/import-meta-url.d.ts +16 -0
  23. package/dist/plugins/import-meta-url.js +193 -0
  24. package/dist/plugins/import-meta-url.js.map +1 -0
  25. package/dist/server/app-browser-action-result.d.ts +9 -16
  26. package/dist/server/app-browser-action-result.js +25 -14
  27. package/dist/server/app-browser-action-result.js.map +1 -1
  28. package/dist/server/app-browser-entry.js +171 -45
  29. package/dist/server/app-browser-entry.js.map +1 -1
  30. package/dist/server/app-browser-mpa-navigation.d.ts +16 -0
  31. package/dist/server/app-browser-mpa-navigation.js +36 -0
  32. package/dist/server/app-browser-mpa-navigation.js.map +1 -0
  33. package/dist/server/app-browser-popstate.d.ts +3 -1
  34. package/dist/server/app-browser-popstate.js +15 -1
  35. package/dist/server/app-browser-popstate.js.map +1 -1
  36. package/dist/server/app-browser-state.js +2 -1
  37. package/dist/server/app-browser-state.js.map +1 -1
  38. package/dist/server/app-layout-param-observation.d.ts +30 -0
  39. package/dist/server/app-layout-param-observation.js +130 -0
  40. package/dist/server/app-layout-param-observation.js.map +1 -0
  41. package/dist/server/app-page-boundary-render.js +2 -2
  42. package/dist/server/app-page-boundary-render.js.map +1 -1
  43. package/dist/server/app-page-dispatch.js +1 -1
  44. package/dist/server/app-page-params.d.ts +2 -1
  45. package/dist/server/app-page-params.js +14 -1
  46. package/dist/server/app-page-params.js.map +1 -1
  47. package/dist/server/app-page-probe.d.ts +12 -1
  48. package/dist/server/app-page-probe.js +116 -1
  49. package/dist/server/app-page-probe.js.map +1 -1
  50. package/dist/server/app-route-handler-response.js +1 -1
  51. package/dist/server/app-route-handler-response.js.map +1 -1
  52. package/dist/server/app-rsc-cache-busting.d.ts +3 -2
  53. package/dist/server/app-rsc-cache-busting.js +9 -7
  54. package/dist/server/app-rsc-cache-busting.js.map +1 -1
  55. package/dist/server/app-rsc-handler.js +11 -1
  56. package/dist/server/app-rsc-handler.js.map +1 -1
  57. package/dist/server/app-segment-config.d.ts +1 -1
  58. package/dist/server/app-segment-config.js +4 -1
  59. package/dist/server/app-segment-config.js.map +1 -1
  60. package/dist/server/app-server-action-execution.d.ts +5 -0
  61. package/dist/server/app-server-action-execution.js +198 -22
  62. package/dist/server/app-server-action-execution.js.map +1 -1
  63. package/dist/server/artifact-compatibility.d.ts +2 -1
  64. package/dist/server/artifact-compatibility.js +10 -1
  65. package/dist/server/artifact-compatibility.js.map +1 -1
  66. package/dist/server/client-reuse-manifest.d.ts +9 -4
  67. package/dist/server/client-reuse-manifest.js +2 -1
  68. package/dist/server/client-reuse-manifest.js.map +1 -1
  69. package/dist/server/dev-server.js +52 -10
  70. package/dist/server/dev-server.js.map +1 -1
  71. package/dist/server/document-initial-head.d.ts +7 -0
  72. package/dist/server/document-initial-head.js +35 -0
  73. package/dist/server/document-initial-head.js.map +1 -0
  74. package/dist/server/pages-document-initial-props.d.ts +84 -2
  75. package/dist/server/pages-document-initial-props.js +127 -1
  76. package/dist/server/pages-document-initial-props.js.map +1 -1
  77. package/dist/server/pages-node-compat.js +1 -1
  78. package/dist/server/pages-page-response.d.ts +14 -0
  79. package/dist/server/pages-page-response.js +31 -8
  80. package/dist/server/pages-page-response.js.map +1 -1
  81. package/dist/server/prod-server.js +13 -6
  82. package/dist/server/prod-server.js.map +1 -1
  83. package/dist/server/skip-cache-proof.d.ts +23 -2
  84. package/dist/server/skip-cache-proof.js +81 -12
  85. package/dist/server/skip-cache-proof.js.map +1 -1
  86. package/dist/server/static-layout-client-reuse-proof.d.ts +16 -0
  87. package/dist/server/static-layout-client-reuse-proof.js +35 -0
  88. package/dist/server/static-layout-client-reuse-proof.js.map +1 -0
  89. package/dist/shims/cache.d.ts +21 -1
  90. package/dist/shims/cache.js +101 -6
  91. package/dist/shims/cache.js.map +1 -1
  92. package/dist/shims/document.d.ts +6 -0
  93. package/dist/shims/document.js +7 -8
  94. package/dist/shims/document.js.map +1 -1
  95. package/dist/shims/error-boundary.d.ts +4 -4
  96. package/dist/shims/error-boundary.js +27 -28
  97. package/dist/shims/error-boundary.js.map +1 -1
  98. package/dist/shims/fetch-cache.d.ts +3 -1
  99. package/dist/shims/fetch-cache.js +16 -5
  100. package/dist/shims/fetch-cache.js.map +1 -1
  101. package/dist/shims/hash-scroll.d.ts +4 -1
  102. package/dist/shims/hash-scroll.js +13 -1
  103. package/dist/shims/hash-scroll.js.map +1 -1
  104. package/dist/shims/head-state.d.ts +1 -0
  105. package/dist/shims/head-state.js +18 -3
  106. package/dist/shims/head-state.js.map +1 -1
  107. package/dist/shims/head.d.ts +35 -1
  108. package/dist/shims/head.js +113 -14
  109. package/dist/shims/head.js.map +1 -1
  110. package/dist/shims/internal/pages-data-fetch-dedup.d.ts +56 -0
  111. package/dist/shims/internal/pages-data-fetch-dedup.js +70 -0
  112. package/dist/shims/internal/pages-data-fetch-dedup.js.map +1 -0
  113. package/dist/shims/link.js +28 -2
  114. package/dist/shims/link.js.map +1 -1
  115. package/dist/shims/navigation.d.ts +39 -1
  116. package/dist/shims/navigation.js +61 -13
  117. package/dist/shims/navigation.js.map +1 -1
  118. package/dist/shims/router.js +37 -17
  119. package/dist/shims/router.js.map +1 -1
  120. package/dist/shims/thenable-params.d.ts +5 -2
  121. package/dist/shims/thenable-params.js +25 -1
  122. package/dist/shims/thenable-params.js.map +1 -1
  123. package/dist/shims/unified-request-context.js +3 -0
  124. package/dist/shims/unified-request-context.js.map +1 -1
  125. package/dist/utils/client-build-manifest.d.ts +15 -0
  126. package/dist/utils/client-build-manifest.js +54 -0
  127. package/dist/utils/client-build-manifest.js.map +1 -0
  128. package/dist/utils/hash.js +1 -1
  129. package/dist/utils/hash.js.map +1 -1
  130. package/dist/utils/lazy-chunks.d.ts +1 -1
  131. package/dist/utils/lazy-chunks.js.map +1 -1
  132. package/dist/utils/vite-version.d.ts +11 -0
  133. package/dist/utils/vite-version.js +36 -0
  134. package/dist/utils/vite-version.js.map +1 -0
  135. package/package.json +2 -2
@@ -10,10 +10,17 @@ import React, { Children, isValidElement, useEffect, useRef } from "react";
10
10
  * document.head projection and applies it with DOM manipulation.
11
11
  */
12
12
  let _ssrHeadChildren = [];
13
+ let _documentInitialHead = [];
14
+ /** @internal — exposed for unit tests of the client head projection. */
13
15
  const _clientHeadChildren = /* @__PURE__ */ new Map();
14
16
  let _getSSRHeadChildren = () => _ssrHeadChildren;
15
17
  let _resetSSRHeadImpl = () => {
16
18
  _ssrHeadChildren = [];
19
+ _documentInitialHead = [];
20
+ };
21
+ let _getDocumentInitialHead = () => _documentInitialHead;
22
+ let _setDocumentInitialHead = (head) => {
23
+ _documentInitialHead = head;
17
24
  };
18
25
  /**
19
26
  * Register ALS-backed state accessors. Called by head-state.ts on import.
@@ -22,14 +29,52 @@ let _resetSSRHeadImpl = () => {
22
29
  function _registerHeadStateAccessors(accessors) {
23
30
  _getSSRHeadChildren = accessors.getSSRHeadChildren;
24
31
  _resetSSRHeadImpl = accessors.resetSSRHead;
32
+ if (accessors.getDocumentInitialHead) _getDocumentInitialHead = accessors.getDocumentInitialHead;
33
+ if (accessors.setDocumentInitialHead) _setDocumentInitialHead = accessors.setDocumentInitialHead;
25
34
  }
26
35
  /** Reset the SSR head collector. Call before render. */
27
36
  function resetSSRHead() {
28
37
  _resetSSRHeadImpl();
29
38
  }
39
+ /**
40
+ * Register head tags returned by a user `_document.getInitialProps()` call.
41
+ * Mirrors Next.js: `_document` may extend the head array passed to its render,
42
+ * and those tags are merged into the final `<head>` output. We treat them the
43
+ * same as `next/head` children — they go through the same dedupe pipeline so
44
+ * later tags (by key or meta-type) win, matching Next.js semantics.
45
+ *
46
+ * Pass an empty array (or simply don't call this) to skip the merge.
47
+ */
48
+ function setDocumentInitialHead(head) {
49
+ _setDocumentInitialHead(head);
50
+ }
51
+ /**
52
+ * Default head tags emitted alongside every Pages Router render — charset
53
+ * first, then viewport. Mirrors Next.js's `defaultHead()` in
54
+ * `packages/next/src/shared/lib/head.tsx`, which seeds the head array used
55
+ * by `HeadManagerContext` before any user `<Head>` reduces over it.
56
+ *
57
+ * The canonical Next.js order is `<meta charset>` then `<meta viewport>`
58
+ * then user tags, all with `data-next-head=""`. See assertion in
59
+ * `test/e2e/next-head/index.test.ts`.
60
+ */
61
+ function defaultHead() {
62
+ return [React.createElement("meta", {
63
+ charSet: "utf-8",
64
+ key: "charset"
65
+ }), React.createElement("meta", {
66
+ name: "viewport",
67
+ content: "width=device-width",
68
+ key: "viewport"
69
+ })];
70
+ }
30
71
  /** Get collected head HTML. Call after render. */
31
72
  function getSSRHeadHTML() {
32
- return reduceHeadChildren(_getSSRHeadChildren()).map((child) => headChildToHTML(child.type, child.props)).filter(Boolean).join("\n ");
73
+ return reduceHeadChildren([
74
+ ...defaultHead(),
75
+ ..._getSSRHeadChildren(),
76
+ ..._getDocumentInitialHead()
77
+ ]).map((child) => headChildToHTML(child.type, child.props)).filter(Boolean).join("\n ");
33
78
  }
34
79
  /**
35
80
  * Tags allowed inside <head>. Anything else is silently dropped.
@@ -138,6 +183,28 @@ function isSafeAttrName(name) {
138
183
  return true;
139
184
  }
140
185
  /**
186
+ * Map React JSX attribute names to their HTML serialised form for the small
187
+ * set of head-relevant attributes where the two differ. React's own renderer
188
+ * normalises these automatically, but we serialise tags by hand so they reach
189
+ * the final HTML in the canonical lowercase / kebab-case shape that browsers
190
+ * (and Next.js's `test/e2e/next-head/index.test.ts`) expect.
191
+ */
192
+ const JSX_TO_HTML_ATTR_MAP = {
193
+ charSet: "charset",
194
+ httpEquiv: "http-equiv",
195
+ acceptCharset: "accept-charset",
196
+ itemProp: "itemprop",
197
+ itemType: "itemtype",
198
+ itemID: "itemid",
199
+ itemRef: "itemref",
200
+ itemScope: "itemscope",
201
+ crossOrigin: "crossorigin",
202
+ referrerPolicy: "referrerpolicy"
203
+ };
204
+ function jsxAttrToHtml(name) {
205
+ return JSX_TO_HTML_ATTR_MAP[name] ?? name;
206
+ }
207
+ /**
141
208
  * Convert props + tag to an HTML string for SSR head injection.
142
209
  * Callers must only pass tags that have already been validated against
143
210
  * ALLOWED_HEAD_TAGS (e.g. via reduceHeadChildren / collectHeadElements).
@@ -153,10 +220,10 @@ function headChildToHTML(tag, props) {
153
220
  else if (key === "className") attrs.push(`class="${escapeAttr(String(value))}"`);
154
221
  else if (typeof value === "string") {
155
222
  if (!isSafeAttrName(key)) continue;
156
- attrs.push(`${key}="${escapeAttr(value)}"`);
223
+ attrs.push(`${jsxAttrToHtml(key)}="${escapeAttr(value)}"`);
157
224
  } else if (typeof value === "boolean" && value) {
158
225
  if (!isSafeAttrName(key)) continue;
159
- attrs.push(key);
226
+ attrs.push(jsxAttrToHtml(key));
160
227
  }
161
228
  const attrStr = attrs.length ? " " + attrs.join(" ") : "";
162
229
  if (SELF_CLOSING_HEAD_TAGS.has(tag)) return `<${tag}${attrStr} data-next-head="" />`;
@@ -197,22 +264,54 @@ function _applyHeadPropsToElement(domEl, props) {
197
264
  else if (key === "className") domEl.setAttribute("class", String(value));
198
265
  else if (typeof value === "boolean" && value) {
199
266
  if (!isSafeAttrName(key)) continue;
200
- domEl.setAttribute(key, "");
267
+ domEl.setAttribute(jsxAttrToHtml(key), "");
201
268
  } else if (typeof value === "string") {
202
269
  if (!isSafeAttrName(key)) continue;
203
- domEl.setAttribute(key, value);
270
+ domEl.setAttribute(jsxAttrToHtml(key), value);
204
271
  }
205
272
  }
206
- function syncClientHead() {
207
- document.querySelectorAll("[data-next-head]").forEach((el) => el.remove());
208
- for (const child of reduceHeadChildren([..._clientHeadChildren.values()])) {
273
+ /**
274
+ * Reconcile the document <head> against the desired projection.
275
+ *
276
+ * Mirrors Next.js's client `head-manager.ts` `updateElements()`: rather than
277
+ * wiping every [data-next-head] node and re-appending (which reorders the
278
+ * SSR-emitted tags to the end of <head> and causes flicker on each update),
279
+ * we diff the desired tags against the existing ones with isEqualNode(). Tags
280
+ * that already match are left untouched in their original DOM position, only
281
+ * genuinely new tags are inserted, and stale tags are removed.
282
+ *
283
+ * The desired list seeds defaultHead() (charset + viewport) ahead of user
284
+ * tags — matching the SSR path in getSSRHeadHTML() and Next.js's
285
+ * reduceComponents(), which always concatenates defaultHead() on both server
286
+ * and client. Without it the first <Head> mount after hydration would drop the
287
+ * server-rendered defaults. Users can still override via key="charset" /
288
+ * key="viewport" through the dedupe pipeline.
289
+ *
290
+ * @internal — exported for unit tests; called from the Head client effect.
291
+ */
292
+ function _syncClientHead() {
293
+ const headEl = document.head;
294
+ if (!headEl) return;
295
+ const oldTags = new Set(headEl.querySelectorAll("[data-next-head]"));
296
+ const charsetEl = headEl.querySelector("meta[charset]");
297
+ if (charsetEl) oldTags.add(charsetEl);
298
+ const newTags = [];
299
+ for (const child of reduceHeadChildren([...defaultHead(), ..._clientHeadChildren.values()])) {
209
300
  if (typeof child.type !== "string") continue;
210
301
  const domEl = document.createElement(child.type);
211
- const props = child.props;
212
- _applyHeadPropsToElement(domEl, props);
302
+ _applyHeadPropsToElement(domEl, child.props);
213
303
  domEl.setAttribute("data-next-head", "");
214
- document.head.appendChild(domEl);
304
+ let isNew = true;
305
+ for (const oldTag of oldTags) if (oldTag.isEqualNode(domEl)) {
306
+ oldTags.delete(oldTag);
307
+ isNew = false;
308
+ break;
309
+ }
310
+ if (isNew) newTags.push(domEl);
215
311
  }
312
+ for (const oldTag of oldTags) oldTag.parentNode?.removeChild(oldTag);
313
+ for (const newTag of newTags) if (newTag.tagName.toLowerCase() === "meta" && newTag.getAttribute("charset") !== null) headEl.prepend(newTag);
314
+ else headEl.appendChild(newTag);
216
315
  }
217
316
  function Head({ children }) {
218
317
  const headInstanceIdRef = useRef(null);
@@ -224,15 +323,15 @@ function Head({ children }) {
224
323
  useEffect(() => {
225
324
  const instanceId = headInstanceIdRef.current;
226
325
  _clientHeadChildren.set(instanceId, children);
227
- syncClientHead();
326
+ _syncClientHead();
228
327
  return () => {
229
328
  _clientHeadChildren.delete(instanceId);
230
- syncClientHead();
329
+ _syncClientHead();
231
330
  };
232
331
  }, [children]);
233
332
  return null;
234
333
  }
235
334
  //#endregion
236
- export { _applyHeadPropsToElement, _registerHeadStateAccessors, Head as default, escapeAttr, escapeInlineContent, getSSRHeadHTML, isSafeAttrName, reduceHeadChildren, resetSSRHead };
335
+ export { _applyHeadPropsToElement, _clientHeadChildren, _registerHeadStateAccessors, _syncClientHead, Head as default, escapeAttr, escapeInlineContent, getSSRHeadHTML, isSafeAttrName, reduceHeadChildren, resetSSRHead, setDocumentInitialHead };
237
336
 
238
337
  //# sourceMappingURL=head.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"head.js","names":[],"sources":["../../src/shims/head.ts"],"sourcesContent":["/**\n * next/head shim\n *\n * In the Pages Router, <Head> manages document <head> elements.\n * - On the server: collects elements into a module-level array that the\n * dev-server reads after render and injects into the HTML <head>.\n * - On the client: reduces all mounted <Head> instances into one deduped\n * document.head projection and applies it with DOM manipulation.\n */\nimport React, { useEffect, useRef, Children, isValidElement } from \"react\";\n\ntype HeadProps = {\n children?: React.ReactNode;\n};\n\n// --- SSR head collection ---\n// State uses a registration pattern so this module can be bundled for the\n// browser. The ALS-backed implementation lives in head-state.ts (server-only).\n\nlet _ssrHeadChildren: React.ReactNode[] = [];\nconst _clientHeadChildren = new Map<symbol, React.ReactNode>();\n\nlet _getSSRHeadChildren = (): React.ReactNode[] => _ssrHeadChildren;\nlet _resetSSRHeadImpl = (): void => {\n _ssrHeadChildren = [];\n};\n\n/**\n * Register ALS-backed state accessors. Called by head-state.ts on import.\n * @internal\n */\nexport function _registerHeadStateAccessors(accessors: {\n getSSRHeadChildren: () => React.ReactNode[];\n resetSSRHead: () => void;\n}): void {\n _getSSRHeadChildren = accessors.getSSRHeadChildren;\n _resetSSRHeadImpl = accessors.resetSSRHead;\n}\n\n/** Reset the SSR head collector. Call before render. */\nexport function resetSSRHead(): void {\n _resetSSRHeadImpl();\n}\n\n/** Get collected head HTML. Call after render. */\nexport function getSSRHeadHTML(): string {\n return reduceHeadChildren(_getSSRHeadChildren())\n .map((child) => headChildToHTML(child.type as string, child.props as Record<string, unknown>))\n .filter(Boolean)\n .join(\"\\n \");\n}\n\n/**\n * Tags allowed inside <head>. Anything else is silently dropped.\n * This prevents injection of dangerous elements like <iframe>, <object>, etc.\n */\nconst ALLOWED_HEAD_TAGS = new Set([\"title\", \"meta\", \"link\", \"style\", \"script\", \"base\", \"noscript\"]);\nconst ALLOWED_HEAD_TAGS_LIST = Array.from(ALLOWED_HEAD_TAGS).join(\", \");\nconst META_TYPES = [\"name\", \"httpEquiv\", \"charSet\", \"itemProp\"] as const;\n\n/** Self-closing tags: no inner content, emit as <tag ... /> */\nconst SELF_CLOSING_HEAD_TAGS = new Set([\"meta\", \"link\", \"base\"]);\n\n/** Tags whose content is raw text — closing-tag sequences must be escaped during SSR. */\nconst RAW_CONTENT_TAGS = new Set([\"script\", \"style\"]);\n\ntype HeadDOMElement = Pick<HTMLElement, \"innerHTML\" | \"setAttribute\" | \"textContent\">;\n\nfunction warnDisallowedHeadTag(tag: string): void {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n `[vinext] <Head> ignoring disallowed tag <${tag}>. ` +\n `Only ${ALLOWED_HEAD_TAGS_LIST} are allowed.`,\n );\n }\n}\n\nfunction collectHeadElements(\n list: React.ReactElement[],\n child: React.ReactNode,\n): React.ReactElement[] {\n if (\n child == null ||\n typeof child === \"boolean\" ||\n typeof child === \"string\" ||\n typeof child === \"number\"\n ) {\n return list;\n }\n if (!isValidElement(child)) {\n return list;\n }\n if (child.type === React.Fragment) {\n return Children.toArray((child.props as { children?: React.ReactNode }).children).reduce(\n collectHeadElements,\n list,\n );\n }\n if (typeof child.type !== \"string\") {\n return list;\n }\n if (!ALLOWED_HEAD_TAGS.has(child.type)) {\n warnDisallowedHeadTag(child.type);\n return list;\n }\n return list.concat(child);\n}\n\nfunction normalizeHeadKey(key: React.Key | null): string | null {\n if (key == null || typeof key === \"number\") return null;\n const normalizedKey = String(key);\n const separatorIndex = normalizedKey.indexOf(\"$\");\n return separatorIndex > 0 ? normalizedKey.slice(separatorIndex + 1) : null;\n}\n\nfunction createUniqueHeadFilter(): (child: React.ReactElement) => boolean {\n const keys = new Set<string>();\n const tags = new Set<string>();\n const metaTypes = new Set<string>();\n const metaCategories = new Map<string, Set<string>>();\n\n return (child) => {\n let isUnique = true;\n const normalizedKey = normalizeHeadKey(child.key);\n const hasKey = normalizedKey !== null;\n if (normalizedKey) {\n if (keys.has(normalizedKey)) {\n isUnique = false;\n } else {\n keys.add(normalizedKey);\n }\n }\n\n switch (child.type) {\n case \"title\":\n case \"base\":\n if (tags.has(child.type)) {\n isUnique = false;\n } else {\n tags.add(child.type);\n }\n break;\n case \"meta\": {\n const props = child.props as Record<string, unknown>;\n for (const metaType of META_TYPES) {\n if (!Object.prototype.hasOwnProperty.call(props, metaType)) continue;\n if (metaType === \"charSet\") {\n if (metaTypes.has(metaType)) {\n isUnique = false;\n } else {\n metaTypes.add(metaType);\n }\n continue;\n }\n\n const category = props[metaType];\n if (typeof category !== \"string\") continue;\n\n let categories = metaCategories.get(metaType);\n if (!categories) {\n categories = new Set<string>();\n metaCategories.set(metaType, categories);\n }\n\n if ((metaType !== \"name\" || !hasKey) && categories.has(category)) {\n isUnique = false;\n } else {\n categories.add(category);\n }\n }\n break;\n }\n default:\n break;\n }\n\n return isUnique;\n };\n}\n\nexport function reduceHeadChildren(headChildren: React.ReactNode[]): React.ReactElement[] {\n return headChildren\n .reduce<React.ReactNode[]>(\n (flattenedChildren, child) => flattenedChildren.concat(Children.toArray(child)),\n [],\n )\n .reduce(collectHeadElements, [])\n .reverse()\n .filter(createUniqueHeadFilter())\n .reverse();\n}\n\n/**\n * Validate an HTML attribute name. Rejects names that could break out of\n * the attribute context during SSR serialization, or that represent inline\n * event handlers (on*). Only allows alphanumeric characters, hyphens, and\n * common data-attribute patterns.\n */\nconst SAFE_ATTR_NAME_RE = /^[a-zA-Z][a-zA-Z0-9\\-:.]*$/;\n\nexport function isSafeAttrName(name: string): boolean {\n if (!SAFE_ATTR_NAME_RE.test(name)) return false;\n // Block inline event handlers (onclick, onerror, etc.)\n if (name.length > 2 && name[0] === \"o\" && name[1] === \"n\" && name[2] >= \"A\" && name[2] <= \"z\")\n return false;\n return true;\n}\n\n/**\n * Convert props + tag to an HTML string for SSR head injection.\n * Callers must only pass tags that have already been validated against\n * ALLOWED_HEAD_TAGS (e.g. via reduceHeadChildren / collectHeadElements).\n */\nfunction headChildToHTML(tag: string, props: Record<string, unknown>): string {\n const attrs: string[] = [];\n let innerHTML = \"\";\n\n // dangerouslySetInnerHTML takes precedence over children, regardless of\n // prop iteration order. Check it first to match Next.js semantics.\n const rawHtml = getDangerouslySetInnerHTML(props.dangerouslySetInnerHTML);\n if (rawHtml != null) {\n // Intentionally raw — developer explicitly opted in.\n // SECURITY NOTE: This injects raw HTML. Developers must never pass\n // unsanitized user input here — it is a stored XSS vector.\n innerHTML = rawHtml;\n } else if (typeof props.children === \"string\") {\n innerHTML = escapeHTML(props.children);\n } else if (Array.isArray(props.children)) {\n innerHTML = escapeHTML(props.children.join(\"\"));\n }\n\n for (const [key, value] of Object.entries(props)) {\n if (key === \"children\" || key === \"dangerouslySetInnerHTML\") {\n continue;\n } else if (key === \"className\") {\n attrs.push(`class=\"${escapeAttr(String(value))}\"`);\n } else if (typeof value === \"string\") {\n if (!isSafeAttrName(key)) continue;\n attrs.push(`${key}=\"${escapeAttr(value)}\"`);\n } else if (typeof value === \"boolean\" && value) {\n if (!isSafeAttrName(key)) continue;\n attrs.push(key);\n }\n }\n\n const attrStr = attrs.length ? \" \" + attrs.join(\" \") : \"\";\n\n if (SELF_CLOSING_HEAD_TAGS.has(tag)) {\n return `<${tag}${attrStr} data-next-head=\"\" />`;\n }\n\n // For raw-content tags (script, style), escape closing-tag sequences so the\n // HTML parser doesn't prematurely terminate the element.\n if (RAW_CONTENT_TAGS.has(tag) && innerHTML) {\n innerHTML = escapeInlineContent(innerHTML, tag);\n }\n\n return `<${tag}${attrStr} data-next-head=\"\">${innerHTML}</${tag}>`;\n}\n\nfunction escapeHTML(s: string): string {\n return s.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nexport function escapeAttr(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\n/**\n * Escape content that will be placed inside a raw <script> or <style> tag\n * during SSR. The HTML parser treats `</script>` (or `</style>`) as the end\n * of the block regardless of JavaScript string context, so any occurrence\n * of `</` followed by the tag name must be escaped.\n *\n * We replace `</script` and `</style` (case-insensitive) with `<\\/script`\n * and `<\\/style` respectively. The `<\\/` form is harmless in JS/CSS string\n * context but prevents the HTML parser from seeing a closing tag.\n */\nexport function escapeInlineContent(content: string, tag: string): string {\n // Build a pattern like `<\\/script` or `<\\/style`, case-insensitive.\n // `tag` is always a literal developer-controlled value (\"script\" or \"style\")\n // guarded by the RAW_CONTENT_TAGS.has(tag) check at all call sites — never user input.\n const pattern = new RegExp(`<\\\\/(${tag})`, \"gi\");\n return content.replace(pattern, \"<\\\\/$1\");\n}\n\nfunction getDangerouslySetInnerHTML(value: unknown): string | undefined {\n if (typeof value !== \"object\" || value === null) return undefined;\n\n const html = Reflect.get(value, \"__html\");\n return typeof html === \"string\" ? html : undefined;\n}\n\nexport function _applyHeadPropsToElement(\n domEl: HeadDOMElement,\n props: Record<string, unknown>,\n): void {\n const rawHtml = getDangerouslySetInnerHTML(props.dangerouslySetInnerHTML);\n\n if (rawHtml != null) {\n domEl.innerHTML = rawHtml;\n } else if (typeof props.children === \"string\") {\n domEl.textContent = props.children;\n } else if (Array.isArray(props.children)) {\n domEl.textContent = props.children.join(\"\");\n }\n\n for (const [key, value] of Object.entries(props)) {\n if (key === \"children\" || key === \"dangerouslySetInnerHTML\") {\n continue;\n } else if (key === \"className\") {\n domEl.setAttribute(\"class\", String(value));\n } else if (typeof value === \"boolean\" && value) {\n if (!isSafeAttrName(key)) continue;\n domEl.setAttribute(key, \"\");\n } else if (typeof value === \"string\") {\n if (!isSafeAttrName(key)) continue;\n domEl.setAttribute(key, value);\n }\n }\n}\n\nfunction syncClientHead(): void {\n document.querySelectorAll(\"[data-next-head]\").forEach((el) => el.remove());\n\n for (const child of reduceHeadChildren([..._clientHeadChildren.values()])) {\n if (typeof child.type !== \"string\") continue;\n\n const domEl = document.createElement(child.type);\n const props = child.props as Record<string, unknown>;\n _applyHeadPropsToElement(domEl, props);\n\n domEl.setAttribute(\"data-next-head\", \"\");\n document.head.appendChild(domEl);\n }\n}\n\n// --- Component ---\n\nfunction Head({ children }: HeadProps): null {\n const headInstanceIdRef = useRef<symbol | null>(null);\n if (headInstanceIdRef.current === null) {\n headInstanceIdRef.current = Symbol(\"vinext-head\");\n }\n\n // SSR path: collect elements for later injection\n if (typeof window === \"undefined\") {\n _getSSRHeadChildren().push(children);\n return null;\n }\n\n // Client path: update the shared head projection after hydration.\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useEffect(() => {\n const instanceId = headInstanceIdRef.current!;\n _clientHeadChildren.set(instanceId, children);\n syncClientHead();\n\n return () => {\n _clientHeadChildren.delete(instanceId);\n syncClientHead();\n };\n }, [children]);\n\n return null;\n}\n\nexport default Head;\n"],"mappings":"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"}
1
+ {"version":3,"file":"head.js","names":[],"sources":["../../src/shims/head.ts"],"sourcesContent":["/**\n * next/head shim\n *\n * In the Pages Router, <Head> manages document <head> elements.\n * - On the server: collects elements into a module-level array that the\n * dev-server reads after render and injects into the HTML <head>.\n * - On the client: reduces all mounted <Head> instances into one deduped\n * document.head projection and applies it with DOM manipulation.\n */\nimport React, { useEffect, useRef, Children, isValidElement } from \"react\";\n\ntype HeadProps = {\n children?: React.ReactNode;\n};\n\n// --- SSR head collection ---\n// State uses a registration pattern so this module can be bundled for the\n// browser. The ALS-backed implementation lives in head-state.ts (server-only).\n\nlet _ssrHeadChildren: React.ReactNode[] = [];\nlet _documentInitialHead: React.ReactNode[] = [];\n/** @internal — exposed for unit tests of the client head projection. */\nexport const _clientHeadChildren = new Map<symbol, React.ReactNode>();\n\nlet _getSSRHeadChildren = (): React.ReactNode[] => _ssrHeadChildren;\nlet _resetSSRHeadImpl = (): void => {\n _ssrHeadChildren = [];\n _documentInitialHead = [];\n};\nlet _getDocumentInitialHead = (): React.ReactNode[] => _documentInitialHead;\nlet _setDocumentInitialHead = (head: React.ReactNode[]): void => {\n _documentInitialHead = head;\n};\n\n/**\n * Register ALS-backed state accessors. Called by head-state.ts on import.\n * @internal\n */\nexport function _registerHeadStateAccessors(accessors: {\n getSSRHeadChildren: () => React.ReactNode[];\n resetSSRHead: () => void;\n getDocumentInitialHead?: () => React.ReactNode[];\n setDocumentInitialHead?: (head: React.ReactNode[]) => void;\n}): void {\n _getSSRHeadChildren = accessors.getSSRHeadChildren;\n _resetSSRHeadImpl = accessors.resetSSRHead;\n if (accessors.getDocumentInitialHead) {\n _getDocumentInitialHead = accessors.getDocumentInitialHead;\n }\n if (accessors.setDocumentInitialHead) {\n _setDocumentInitialHead = accessors.setDocumentInitialHead;\n }\n}\n\n/** Reset the SSR head collector. Call before render. */\nexport function resetSSRHead(): void {\n _resetSSRHeadImpl();\n}\n\n/**\n * Register head tags returned by a user `_document.getInitialProps()` call.\n * Mirrors Next.js: `_document` may extend the head array passed to its render,\n * and those tags are merged into the final `<head>` output. We treat them the\n * same as `next/head` children — they go through the same dedupe pipeline so\n * later tags (by key or meta-type) win, matching Next.js semantics.\n *\n * Pass an empty array (or simply don't call this) to skip the merge.\n */\nexport function setDocumentInitialHead(head: React.ReactNode[]): void {\n _setDocumentInitialHead(head);\n}\n\n/**\n * Default head tags emitted alongside every Pages Router render — charset\n * first, then viewport. Mirrors Next.js's `defaultHead()` in\n * `packages/next/src/shared/lib/head.tsx`, which seeds the head array used\n * by `HeadManagerContext` before any user `<Head>` reduces over it.\n *\n * The canonical Next.js order is `<meta charset>` then `<meta viewport>`\n * then user tags, all with `data-next-head=\"\"`. See assertion in\n * `test/e2e/next-head/index.test.ts`.\n */\nfunction defaultHead(): React.ReactElement[] {\n return [\n React.createElement(\"meta\", { charSet: \"utf-8\", key: \"charset\" }),\n React.createElement(\"meta\", {\n name: \"viewport\",\n content: \"width=device-width\",\n key: \"viewport\",\n }),\n ];\n}\n\n/** Get collected head HTML. Call after render. */\nexport function getSSRHeadHTML(): string {\n // Order mirrors Next.js's `_document.tsx`: defaultHead seeds the head array,\n // user `next/head` tags reduce over it, and then `_document.getInitialProps`\n // may extend the array. The final `_document` render emits `{head}` (which\n // contains the defaults + user tags + initial-props tags) ahead of any\n // children declared inside `_document`'s own `<Head>`. Because the user\n // children inside `_document`'s `<Head>` are tracked via React tree render\n // (not next/head), they don't appear in this collector — so emitting\n // `defaultHead + user + initialProps` here matches Next.js's serialised\n // output up to that boundary.\n return reduceHeadChildren([\n ...defaultHead(),\n ..._getSSRHeadChildren(),\n ..._getDocumentInitialHead(),\n ])\n .map((child) => headChildToHTML(child.type as string, child.props as Record<string, unknown>))\n .filter(Boolean)\n .join(\"\\n \");\n}\n\n/**\n * Tags allowed inside <head>. Anything else is silently dropped.\n * This prevents injection of dangerous elements like <iframe>, <object>, etc.\n */\nconst ALLOWED_HEAD_TAGS = new Set([\"title\", \"meta\", \"link\", \"style\", \"script\", \"base\", \"noscript\"]);\nconst ALLOWED_HEAD_TAGS_LIST = Array.from(ALLOWED_HEAD_TAGS).join(\", \");\nconst META_TYPES = [\"name\", \"httpEquiv\", \"charSet\", \"itemProp\"] as const;\n\n/** Self-closing tags: no inner content, emit as <tag ... /> */\nconst SELF_CLOSING_HEAD_TAGS = new Set([\"meta\", \"link\", \"base\"]);\n\n/** Tags whose content is raw text — closing-tag sequences must be escaped during SSR. */\nconst RAW_CONTENT_TAGS = new Set([\"script\", \"style\"]);\n\ntype HeadDOMElement = Pick<HTMLElement, \"innerHTML\" | \"setAttribute\" | \"textContent\">;\n\nfunction warnDisallowedHeadTag(tag: string): void {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n `[vinext] <Head> ignoring disallowed tag <${tag}>. ` +\n `Only ${ALLOWED_HEAD_TAGS_LIST} are allowed.`,\n );\n }\n}\n\nfunction collectHeadElements(\n list: React.ReactElement[],\n child: React.ReactNode,\n): React.ReactElement[] {\n if (\n child == null ||\n typeof child === \"boolean\" ||\n typeof child === \"string\" ||\n typeof child === \"number\"\n ) {\n return list;\n }\n if (!isValidElement(child)) {\n return list;\n }\n if (child.type === React.Fragment) {\n return Children.toArray((child.props as { children?: React.ReactNode }).children).reduce(\n collectHeadElements,\n list,\n );\n }\n if (typeof child.type !== \"string\") {\n return list;\n }\n if (!ALLOWED_HEAD_TAGS.has(child.type)) {\n warnDisallowedHeadTag(child.type);\n return list;\n }\n return list.concat(child);\n}\n\nfunction normalizeHeadKey(key: React.Key | null): string | null {\n if (key == null || typeof key === \"number\") return null;\n const normalizedKey = String(key);\n const separatorIndex = normalizedKey.indexOf(\"$\");\n return separatorIndex > 0 ? normalizedKey.slice(separatorIndex + 1) : null;\n}\n\nfunction createUniqueHeadFilter(): (child: React.ReactElement) => boolean {\n const keys = new Set<string>();\n const tags = new Set<string>();\n const metaTypes = new Set<string>();\n const metaCategories = new Map<string, Set<string>>();\n\n return (child) => {\n let isUnique = true;\n const normalizedKey = normalizeHeadKey(child.key);\n const hasKey = normalizedKey !== null;\n if (normalizedKey) {\n if (keys.has(normalizedKey)) {\n isUnique = false;\n } else {\n keys.add(normalizedKey);\n }\n }\n\n switch (child.type) {\n case \"title\":\n case \"base\":\n if (tags.has(child.type)) {\n isUnique = false;\n } else {\n tags.add(child.type);\n }\n break;\n case \"meta\": {\n const props = child.props as Record<string, unknown>;\n for (const metaType of META_TYPES) {\n if (!Object.prototype.hasOwnProperty.call(props, metaType)) continue;\n if (metaType === \"charSet\") {\n if (metaTypes.has(metaType)) {\n isUnique = false;\n } else {\n metaTypes.add(metaType);\n }\n continue;\n }\n\n const category = props[metaType];\n if (typeof category !== \"string\") continue;\n\n let categories = metaCategories.get(metaType);\n if (!categories) {\n categories = new Set<string>();\n metaCategories.set(metaType, categories);\n }\n\n if ((metaType !== \"name\" || !hasKey) && categories.has(category)) {\n isUnique = false;\n } else {\n categories.add(category);\n }\n }\n break;\n }\n default:\n break;\n }\n\n return isUnique;\n };\n}\n\nexport function reduceHeadChildren(headChildren: React.ReactNode[]): React.ReactElement[] {\n return headChildren\n .reduce<React.ReactNode[]>(\n (flattenedChildren, child) => flattenedChildren.concat(Children.toArray(child)),\n [],\n )\n .reduce(collectHeadElements, [])\n .reverse()\n .filter(createUniqueHeadFilter())\n .reverse();\n}\n\n/**\n * Validate an HTML attribute name. Rejects names that could break out of\n * the attribute context during SSR serialization, or that represent inline\n * event handlers (on*). Only allows alphanumeric characters, hyphens, and\n * common data-attribute patterns.\n */\nconst SAFE_ATTR_NAME_RE = /^[a-zA-Z][a-zA-Z0-9\\-:.]*$/;\n\nexport function isSafeAttrName(name: string): boolean {\n if (!SAFE_ATTR_NAME_RE.test(name)) return false;\n // Block inline event handlers (onclick, onerror, etc.)\n if (name.length > 2 && name[0] === \"o\" && name[1] === \"n\" && name[2] >= \"A\" && name[2] <= \"z\")\n return false;\n return true;\n}\n\n/**\n * Map React JSX attribute names to their HTML serialised form for the small\n * set of head-relevant attributes where the two differ. React's own renderer\n * normalises these automatically, but we serialise tags by hand so they reach\n * the final HTML in the canonical lowercase / kebab-case shape that browsers\n * (and Next.js's `test/e2e/next-head/index.test.ts`) expect.\n */\nconst JSX_TO_HTML_ATTR_MAP: Record<string, string> = {\n charSet: \"charset\",\n httpEquiv: \"http-equiv\",\n acceptCharset: \"accept-charset\",\n itemProp: \"itemprop\",\n itemType: \"itemtype\",\n itemID: \"itemid\",\n itemRef: \"itemref\",\n itemScope: \"itemscope\",\n crossOrigin: \"crossorigin\",\n referrerPolicy: \"referrerpolicy\",\n};\n\nfunction jsxAttrToHtml(name: string): string {\n return JSX_TO_HTML_ATTR_MAP[name] ?? name;\n}\n\n/**\n * Convert props + tag to an HTML string for SSR head injection.\n * Callers must only pass tags that have already been validated against\n * ALLOWED_HEAD_TAGS (e.g. via reduceHeadChildren / collectHeadElements).\n */\nfunction headChildToHTML(tag: string, props: Record<string, unknown>): string {\n const attrs: string[] = [];\n let innerHTML = \"\";\n\n // dangerouslySetInnerHTML takes precedence over children, regardless of\n // prop iteration order. Check it first to match Next.js semantics.\n const rawHtml = getDangerouslySetInnerHTML(props.dangerouslySetInnerHTML);\n if (rawHtml != null) {\n // Intentionally raw — developer explicitly opted in.\n // SECURITY NOTE: This injects raw HTML. Developers must never pass\n // unsanitized user input here — it is a stored XSS vector.\n innerHTML = rawHtml;\n } else if (typeof props.children === \"string\") {\n innerHTML = escapeHTML(props.children);\n } else if (Array.isArray(props.children)) {\n innerHTML = escapeHTML(props.children.join(\"\"));\n }\n\n for (const [key, value] of Object.entries(props)) {\n if (key === \"children\" || key === \"dangerouslySetInnerHTML\") {\n continue;\n } else if (key === \"className\") {\n attrs.push(`class=\"${escapeAttr(String(value))}\"`);\n } else if (typeof value === \"string\") {\n if (!isSafeAttrName(key)) continue;\n attrs.push(`${jsxAttrToHtml(key)}=\"${escapeAttr(value)}\"`);\n } else if (typeof value === \"boolean\" && value) {\n if (!isSafeAttrName(key)) continue;\n attrs.push(jsxAttrToHtml(key));\n }\n }\n\n const attrStr = attrs.length ? \" \" + attrs.join(\" \") : \"\";\n\n if (SELF_CLOSING_HEAD_TAGS.has(tag)) {\n return `<${tag}${attrStr} data-next-head=\"\" />`;\n }\n\n // For raw-content tags (script, style), escape closing-tag sequences so the\n // HTML parser doesn't prematurely terminate the element.\n if (RAW_CONTENT_TAGS.has(tag) && innerHTML) {\n innerHTML = escapeInlineContent(innerHTML, tag);\n }\n\n return `<${tag}${attrStr} data-next-head=\"\">${innerHTML}</${tag}>`;\n}\n\nfunction escapeHTML(s: string): string {\n return s.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nexport function escapeAttr(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\n/**\n * Escape content that will be placed inside a raw <script> or <style> tag\n * during SSR. The HTML parser treats `</script>` (or `</style>`) as the end\n * of the block regardless of JavaScript string context, so any occurrence\n * of `</` followed by the tag name must be escaped.\n *\n * We replace `</script` and `</style` (case-insensitive) with `<\\/script`\n * and `<\\/style` respectively. The `<\\/` form is harmless in JS/CSS string\n * context but prevents the HTML parser from seeing a closing tag.\n */\nexport function escapeInlineContent(content: string, tag: string): string {\n // Build a pattern like `<\\/script` or `<\\/style`, case-insensitive.\n // `tag` is always a literal developer-controlled value (\"script\" or \"style\")\n // guarded by the RAW_CONTENT_TAGS.has(tag) check at all call sites — never user input.\n const pattern = new RegExp(`<\\\\/(${tag})`, \"gi\");\n return content.replace(pattern, \"<\\\\/$1\");\n}\n\nfunction getDangerouslySetInnerHTML(value: unknown): string | undefined {\n if (typeof value !== \"object\" || value === null) return undefined;\n\n const html = Reflect.get(value, \"__html\");\n return typeof html === \"string\" ? html : undefined;\n}\n\nexport function _applyHeadPropsToElement(\n domEl: HeadDOMElement,\n props: Record<string, unknown>,\n): void {\n const rawHtml = getDangerouslySetInnerHTML(props.dangerouslySetInnerHTML);\n\n if (rawHtml != null) {\n domEl.innerHTML = rawHtml;\n } else if (typeof props.children === \"string\") {\n domEl.textContent = props.children;\n } else if (Array.isArray(props.children)) {\n domEl.textContent = props.children.join(\"\");\n }\n\n for (const [key, value] of Object.entries(props)) {\n if (key === \"children\" || key === \"dangerouslySetInnerHTML\") {\n continue;\n } else if (key === \"className\") {\n domEl.setAttribute(\"class\", String(value));\n } else if (typeof value === \"boolean\" && value) {\n if (!isSafeAttrName(key)) continue;\n // Map JSX attribute names (charSet, httpEquiv, ...) to the HTML form\n // (charset, http-equiv, ...) so the client-side mutation matches the\n // SSR output. `setAttribute` is case-insensitive for HTML elements, so\n // `charSet` would land as `charset` by coincidence, but `httpEquiv`\n // would lowercase to `httpequiv` rather than `http-equiv` and produce\n // a hydration mismatch. The shared mapping in jsxAttrToHtml keeps both\n // paths in lockstep.\n domEl.setAttribute(jsxAttrToHtml(key), \"\");\n } else if (typeof value === \"string\") {\n if (!isSafeAttrName(key)) continue;\n domEl.setAttribute(jsxAttrToHtml(key), value);\n }\n }\n}\n\n/**\n * Reconcile the document <head> against the desired projection.\n *\n * Mirrors Next.js's client `head-manager.ts` `updateElements()`: rather than\n * wiping every [data-next-head] node and re-appending (which reorders the\n * SSR-emitted tags to the end of <head> and causes flicker on each update),\n * we diff the desired tags against the existing ones with isEqualNode(). Tags\n * that already match are left untouched in their original DOM position, only\n * genuinely new tags are inserted, and stale tags are removed.\n *\n * The desired list seeds defaultHead() (charset + viewport) ahead of user\n * tags — matching the SSR path in getSSRHeadHTML() and Next.js's\n * reduceComponents(), which always concatenates defaultHead() on both server\n * and client. Without it the first <Head> mount after hydration would drop the\n * server-rendered defaults. Users can still override via key=\"charset\" /\n * key=\"viewport\" through the dedupe pipeline.\n *\n * @internal — exported for unit tests; called from the Head client effect.\n */\nexport function _syncClientHead(): void {\n const headEl = document.head;\n if (!headEl) return;\n\n // Existing vinext-managed tags. Also fold in any <meta charset> even if it\n // somehow lost the marker, so we never end up with a duplicate charset.\n const oldTags = new Set<Element>(headEl.querySelectorAll(\"[data-next-head]\"));\n const charsetEl = headEl.querySelector(\"meta[charset]\");\n if (charsetEl) oldTags.add(charsetEl);\n\n const newTags: Element[] = [];\n for (const child of reduceHeadChildren([...defaultHead(), ..._clientHeadChildren.values()])) {\n if (typeof child.type !== \"string\") continue;\n\n const domEl = document.createElement(child.type);\n _applyHeadPropsToElement(domEl, child.props as Record<string, unknown>);\n domEl.setAttribute(\"data-next-head\", \"\");\n\n // Reuse an identical node already in <head> so its DOM position (and thus\n // the head ordering produced by SSR) is preserved.\n //\n // Note: Next.js routes <title> through document.title rather than\n // updateElements(), so a title node never moves. We reconcile <title> like\n // any other tag — on hydration with an unchanged title it is reused in\n // place via isEqualNode (the common case), and only on a client-side title\n // *change* does the old node get removed and the new one appended. The\n // position of <title> in <head> is not observable, so this is cosmetic.\n let isNew = true;\n for (const oldTag of oldTags) {\n if (oldTag.isEqualNode(domEl)) {\n oldTags.delete(oldTag);\n isNew = false;\n break;\n }\n }\n if (isNew) newTags.push(domEl);\n }\n\n // Remove tags that are no longer desired.\n for (const oldTag of oldTags) {\n oldTag.parentNode?.removeChild(oldTag);\n }\n\n // Insert genuinely new tags. Keep <meta charset> first in <head> so the\n // declared encoding stays at the top.\n //\n // This deliberately diverges from Next.js's literal head-manager.ts, which\n // does `if (charset) headEl.prepend(newTag)` with NO `else` and then\n // unconditionally `headEl.appendChild(newTag)` — moving a newly-created\n // charset node twice and landing it last. We use a proper if/else so a new\n // charset is prepended and only prepended. Don't \"fix\" this back to match\n // Next.js's sequence. (This branch only runs on client-only navigation; on\n // SSR hydration the charset is reused in place via isEqualNode above.)\n for (const newTag of newTags) {\n if (newTag.tagName.toLowerCase() === \"meta\" && newTag.getAttribute(\"charset\") !== null) {\n headEl.prepend(newTag);\n } else {\n headEl.appendChild(newTag);\n }\n }\n}\n\n// --- Component ---\n\nfunction Head({ children }: HeadProps): null {\n const headInstanceIdRef = useRef<symbol | null>(null);\n if (headInstanceIdRef.current === null) {\n headInstanceIdRef.current = Symbol(\"vinext-head\");\n }\n\n // SSR path: collect elements for later injection\n if (typeof window === \"undefined\") {\n _getSSRHeadChildren().push(children);\n return null;\n }\n\n // Client path: update the shared head projection after hydration.\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useEffect(() => {\n const instanceId = headInstanceIdRef.current!;\n _clientHeadChildren.set(instanceId, children);\n _syncClientHead();\n\n return () => {\n _clientHeadChildren.delete(instanceId);\n _syncClientHead();\n };\n }, [children]);\n\n return null;\n}\n\nexport default Head;\n"],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ //#region src/shims/internal/pages-data-fetch-dedup.d.ts
2
+ /**
3
+ * In-flight request dedup for the Pages Router `/_next/data/<id>/<page>.json`
4
+ * endpoint.
5
+ *
6
+ * Why this exists: when a user (or app code) triggers several near-simultaneous
7
+ * navigations to the same gSSP route — e.g. clicking the same `<Link>` multiple
8
+ * times before the first navigation lands — each call to `Router.push` would
9
+ * otherwise enter its own `navigateClientData()` flow and dispatch its own
10
+ * `fetch()` against the data endpoint. That balloons server load and breaks
11
+ * Next.js' documented "one fetch per unique data URL" guarantee.
12
+ *
13
+ * Ported from Next.js: `fetchNextData()` in
14
+ * `packages/next/src/shared/lib/router/router.ts`. Next.js maintains an
15
+ * `inflightCache` (keyed by the resolved data URL) and reuses the existing
16
+ * Promise when a concurrent caller asks for the same URL. The entry is
17
+ * dropped once the fetch settles (success or rejection) so the next
18
+ * navigation re-fetches fresh.
19
+ *
20
+ * Design notes:
21
+ *
22
+ * - Callers receive a cloned Response, so each can independently consume the
23
+ * body (`.json()`, `.text()`, etc.). The originating Response is never read
24
+ * directly by anyone, which keeps subsequent clones legal even after one
25
+ * caller has consumed its copy.
26
+ *
27
+ * - No `AbortSignal` is honored at the shared layer. Each `Router.push` cycle
28
+ * has its own AbortController that supersedes prior navigations via
29
+ * `_navigationId`; aborting the shared fetch on behalf of one caller would
30
+ * destroy the dedup gain for every other concurrent caller. Cancellation is
31
+ * handled by the caller's `assertStillCurrent()` checkpoints after `await`,
32
+ * not by abort propagation.
33
+ *
34
+ * - The map is module-scoped (one per realm). The Pages Router runs in the
35
+ * browser only, so a single `Map` is sufficient.
36
+ */
37
+ /**
38
+ * Dedupe a `fetch()` against the `_next/data` endpoint. Multiple concurrent
39
+ * callers for the same `dataHref` share one underlying network request.
40
+ *
41
+ * Each call returns a freshly-cloned `Response` so consumers can read the
42
+ * body independently. Once the in-flight Promise settles (resolve or reject)
43
+ * the entry is removed, and the next call will hit the network again.
44
+ *
45
+ * Errors propagate to every concurrent caller — the in-flight entry is
46
+ * dropped on failure so the next navigation can retry.
47
+ */
48
+ declare function dedupedPagesDataFetch(dataHref: string, init?: RequestInit): Promise<Response>;
49
+ /**
50
+ * Drop every cached in-flight entry. Intended for tests; production code
51
+ * does not need to call this because entries self-evict on settle.
52
+ */
53
+ declare function clearPagesDataInflight(): void;
54
+ //#endregion
55
+ export { clearPagesDataInflight, dedupedPagesDataFetch };
56
+ //# sourceMappingURL=pages-data-fetch-dedup.d.ts.map
@@ -0,0 +1,70 @@
1
+ //#region src/shims/internal/pages-data-fetch-dedup.ts
2
+ /**
3
+ * In-flight request dedup for the Pages Router `/_next/data/<id>/<page>.json`
4
+ * endpoint.
5
+ *
6
+ * Why this exists: when a user (or app code) triggers several near-simultaneous
7
+ * navigations to the same gSSP route — e.g. clicking the same `<Link>` multiple
8
+ * times before the first navigation lands — each call to `Router.push` would
9
+ * otherwise enter its own `navigateClientData()` flow and dispatch its own
10
+ * `fetch()` against the data endpoint. That balloons server load and breaks
11
+ * Next.js' documented "one fetch per unique data URL" guarantee.
12
+ *
13
+ * Ported from Next.js: `fetchNextData()` in
14
+ * `packages/next/src/shared/lib/router/router.ts`. Next.js maintains an
15
+ * `inflightCache` (keyed by the resolved data URL) and reuses the existing
16
+ * Promise when a concurrent caller asks for the same URL. The entry is
17
+ * dropped once the fetch settles (success or rejection) so the next
18
+ * navigation re-fetches fresh.
19
+ *
20
+ * Design notes:
21
+ *
22
+ * - Callers receive a cloned Response, so each can independently consume the
23
+ * body (`.json()`, `.text()`, etc.). The originating Response is never read
24
+ * directly by anyone, which keeps subsequent clones legal even after one
25
+ * caller has consumed its copy.
26
+ *
27
+ * - No `AbortSignal` is honored at the shared layer. Each `Router.push` cycle
28
+ * has its own AbortController that supersedes prior navigations via
29
+ * `_navigationId`; aborting the shared fetch on behalf of one caller would
30
+ * destroy the dedup gain for every other concurrent caller. Cancellation is
31
+ * handled by the caller's `assertStillCurrent()` checkpoints after `await`,
32
+ * not by abort propagation.
33
+ *
34
+ * - The map is module-scoped (one per realm). The Pages Router runs in the
35
+ * browser only, so a single `Map` is sufficient.
36
+ */
37
+ /** Inflight fetch promises keyed by the resolved data URL. */
38
+ const inflight = /* @__PURE__ */ new Map();
39
+ /**
40
+ * Dedupe a `fetch()` against the `_next/data` endpoint. Multiple concurrent
41
+ * callers for the same `dataHref` share one underlying network request.
42
+ *
43
+ * Each call returns a freshly-cloned `Response` so consumers can read the
44
+ * body independently. Once the in-flight Promise settles (resolve or reject)
45
+ * the entry is removed, and the next call will hit the network again.
46
+ *
47
+ * Errors propagate to every concurrent caller — the in-flight entry is
48
+ * dropped on failure so the next navigation can retry.
49
+ */
50
+ function dedupedPagesDataFetch(dataHref, init) {
51
+ let entry = inflight.get(dataHref);
52
+ if (!entry) {
53
+ entry = fetch(dataHref, init).finally(() => {
54
+ if (inflight.get(dataHref) === entry) inflight.delete(dataHref);
55
+ });
56
+ inflight.set(dataHref, entry);
57
+ }
58
+ return entry.then((res) => res.clone());
59
+ }
60
+ /**
61
+ * Drop every cached in-flight entry. Intended for tests; production code
62
+ * does not need to call this because entries self-evict on settle.
63
+ */
64
+ function clearPagesDataInflight() {
65
+ inflight.clear();
66
+ }
67
+ //#endregion
68
+ export { clearPagesDataInflight, dedupedPagesDataFetch };
69
+
70
+ //# sourceMappingURL=pages-data-fetch-dedup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages-data-fetch-dedup.js","names":[],"sources":["../../../src/shims/internal/pages-data-fetch-dedup.ts"],"sourcesContent":["/**\n * In-flight request dedup for the Pages Router `/_next/data/<id>/<page>.json`\n * endpoint.\n *\n * Why this exists: when a user (or app code) triggers several near-simultaneous\n * navigations to the same gSSP route — e.g. clicking the same `<Link>` multiple\n * times before the first navigation lands — each call to `Router.push` would\n * otherwise enter its own `navigateClientData()` flow and dispatch its own\n * `fetch()` against the data endpoint. That balloons server load and breaks\n * Next.js' documented \"one fetch per unique data URL\" guarantee.\n *\n * Ported from Next.js: `fetchNextData()` in\n * `packages/next/src/shared/lib/router/router.ts`. Next.js maintains an\n * `inflightCache` (keyed by the resolved data URL) and reuses the existing\n * Promise when a concurrent caller asks for the same URL. The entry is\n * dropped once the fetch settles (success or rejection) so the next\n * navigation re-fetches fresh.\n *\n * Design notes:\n *\n * - Callers receive a cloned Response, so each can independently consume the\n * body (`.json()`, `.text()`, etc.). The originating Response is never read\n * directly by anyone, which keeps subsequent clones legal even after one\n * caller has consumed its copy.\n *\n * - No `AbortSignal` is honored at the shared layer. Each `Router.push` cycle\n * has its own AbortController that supersedes prior navigations via\n * `_navigationId`; aborting the shared fetch on behalf of one caller would\n * destroy the dedup gain for every other concurrent caller. Cancellation is\n * handled by the caller's `assertStillCurrent()` checkpoints after `await`,\n * not by abort propagation.\n *\n * - The map is module-scoped (one per realm). The Pages Router runs in the\n * browser only, so a single `Map` is sufficient.\n */\n\n/** Inflight fetch promises keyed by the resolved data URL. */\nconst inflight = new Map<string, Promise<Response>>();\n\n/**\n * Dedupe a `fetch()` against the `_next/data` endpoint. Multiple concurrent\n * callers for the same `dataHref` share one underlying network request.\n *\n * Each call returns a freshly-cloned `Response` so consumers can read the\n * body independently. Once the in-flight Promise settles (resolve or reject)\n * the entry is removed, and the next call will hit the network again.\n *\n * Errors propagate to every concurrent caller — the in-flight entry is\n * dropped on failure so the next navigation can retry.\n */\nexport function dedupedPagesDataFetch(dataHref: string, init?: RequestInit): Promise<Response> {\n let entry = inflight.get(dataHref);\n if (!entry) {\n entry = fetch(dataHref, init).finally(() => {\n // Only drop the entry if it still matches the one we set. A racing\n // caller could in principle overwrite, but inflight is keyed by URL\n // and we never overwrite on a hit, so this check is defensive.\n if (inflight.get(dataHref) === entry) inflight.delete(dataHref);\n });\n inflight.set(dataHref, entry);\n }\n // Always return a clone so each consumer gets an independently-readable\n // body. The original `entry` Response is never consumed directly, so\n // cloning remains valid for every caller (including the first).\n return entry.then((res) => res.clone());\n}\n\n/**\n * Drop every cached in-flight entry. Intended for tests; production code\n * does not need to call this because entries self-evict on settle.\n */\nexport function clearPagesDataInflight(): void {\n inflight.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,MAAM,2BAAW,IAAI,KAAgC;;;;;;;;;;;;AAarD,SAAgB,sBAAsB,UAAkB,MAAuC;CAC7F,IAAI,QAAQ,SAAS,IAAI,SAAS;CAClC,IAAI,CAAC,OAAO;EACV,QAAQ,MAAM,UAAU,KAAK,CAAC,cAAc;GAI1C,IAAI,SAAS,IAAI,SAAS,KAAK,OAAO,SAAS,OAAO,SAAS;IAC/D;EACF,SAAS,IAAI,UAAU,MAAM;;CAK/B,OAAO,MAAM,MAAM,QAAQ,IAAI,OAAO,CAAC;;;;;;AAOzC,SAAgB,yBAA+B;CAC7C,SAAS,OAAO"}
@@ -41,6 +41,7 @@ function useLinkStatus() {
41
41
  const __basePath = process.env.__NEXT_ROUTER_BASEPATH ?? "";
42
42
  /** trailingSlash from next.config.js, injected by the plugin at build time */
43
43
  const __trailingSlash = process.env.__VINEXT_TRAILING_SLASH === "true";
44
+ const __prefetchInlining = process.env.__VINEXT_PREFETCH_INLINING === "true";
44
45
  const linkPrefetchRouteTrieCache = createRouteTrieCache();
45
46
  function resolveHref(href) {
46
47
  if (typeof href === "string") return href;
@@ -168,7 +169,11 @@ function prefetchUrl(href, mode, priority = "low") {
168
169
  });
169
170
  if (prefetchHref == null) return;
170
171
  const fullHref = toBrowserNavigationHref(prefetchHref, window.location.href, __basePath);
171
- (window.requestIdleCallback ?? ((fn) => setTimeout(fn, 100)))(() => {
172
+ const target = new URL(fullHref, window.location.href);
173
+ if (target.origin === window.location.origin && target.pathname === window.location.pathname && target.search === window.location.search) return;
174
+ (priority === "high" ? (fn) => {
175
+ fn();
176
+ } : window.requestIdleCallback ?? ((fn) => setTimeout(fn, 100)))(() => {
172
177
  (async () => {
173
178
  if (hasAppNavigationRuntime()) {
174
179
  const autoPrefetch = mode === "auto" ? resolveAutoAppRoutePrefetch(prefetchHref) : {
@@ -196,7 +201,28 @@ function prefetchUrl(href, mode, priority = "low") {
196
201
  return;
197
202
  }
198
203
  prefetched.add(cacheKey);
199
- prefetchRscResponse(rscUrl, fetch(rscUrl, {
204
+ prefetchRscResponse(rscUrl, __prefetchInlining && autoPrefetch.cacheForNavigation ? (async () => {
205
+ const shellHeaders = createRscRequestHeaders({
206
+ interceptionContext,
207
+ renderMode: APP_RSC_RENDER_MODE_PREFETCH_LOADING_SHELL
208
+ });
209
+ if (mountedSlotsHeader) shellHeaders.set(VINEXT_MOUNTED_SLOTS_HEADER, mountedSlotsHeader);
210
+ const shellRscUrl = await createRscRequestUrl(fullHref, shellHeaders);
211
+ const shellResponse = await fetch(shellRscUrl, {
212
+ headers: shellHeaders,
213
+ credentials: "include",
214
+ priority,
215
+ purpose: "prefetch"
216
+ });
217
+ if (!shellResponse.ok) return shellResponse;
218
+ await shellResponse.arrayBuffer().catch(() => {});
219
+ return fetch(rscUrl, {
220
+ headers,
221
+ credentials: "include",
222
+ priority,
223
+ purpose: "prefetch"
224
+ });
225
+ })() : fetch(rscUrl, {
200
226
  headers,
201
227
  credentials: "include",
202
228
  priority,