@wular/pnext 0.0.1

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 (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,974 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { createHash } from 'node:crypto';
3
+ import { createRequire } from 'node:module';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import type { ResolvedConfig } from '../../../config';
9
+ import { onExtensionHostReset, registerResponseFinalizers } from '../../../extensions';
10
+ import { getWorkUnit } from '../../../request/context';
11
+ import type { MetadataLink } from '../../../types';
12
+ import { persistFont, persistedFont } from './cache';
13
+ import {
14
+ familyStack,
15
+ fontStyle,
16
+ googleFontNaming,
17
+ localFontNaming,
18
+ singleQuoteFamily,
19
+ stableJson,
20
+ type FontDisplay,
21
+ type GoogleFontOptions,
22
+ type LocalFontOptions,
23
+ type LocalFontSource,
24
+ type NextFont,
25
+ type NextFontWithVariable,
26
+ } from './shared';
27
+
28
+ export type {
29
+ FontDisplay,
30
+ GoogleFontOptions,
31
+ LocalFontOptions,
32
+ LocalFontSource,
33
+ NextFont,
34
+ NextFontWithVariable,
35
+ };
36
+
37
+ interface FontScope {
38
+ fonts: Map<string, FontDefinition>;
39
+ }
40
+
41
+ interface FontDefinition {
42
+ className: string;
43
+ family: string;
44
+ key: string;
45
+ resolve(context: FontResolveContext): Promise<ResolvedFont>;
46
+ style: NextFont['style'];
47
+ variable: string;
48
+ hasVariable: boolean;
49
+ }
50
+
51
+ interface FontResolveContext {
52
+ config: ResolvedConfig;
53
+ dev?: boolean;
54
+ }
55
+
56
+ export interface ResolvedFontAssets {
57
+ css: string;
58
+ preloads: MetadataLink[];
59
+ }
60
+
61
+ interface ResolvedFont {
62
+ css: string;
63
+ /** Preloadable font links (empty when `preload:false`). Delivered via the
64
+ * `Link` response header in prod, not as HTML `<link>` tags. */
65
+ preloads: MetadataLink[];
66
+ /** Whether this definition emitted any font file (used to decide preconnect). */
67
+ usesFonts: boolean;
68
+ /** Emitted font files on disk, re-checked on every resolve — a rebuild can
69
+ * wipe the output directory under a running server. */
70
+ files: string[];
71
+ /** Local source files this resolution read; the persisted record stats them. */
72
+ sources?: string[];
73
+ }
74
+
75
+ interface GoogleFontMetadata {
76
+ axes?: { tag: string; min: number; max: number; defaultValue?: number }[];
77
+ styles: string[];
78
+ subsets: string[];
79
+ weights: string[];
80
+ }
81
+
82
+ interface NormalizedGoogleOptions {
83
+ axes?: string[];
84
+ adjustFontFallback: boolean;
85
+ display: FontDisplay;
86
+ fallback: string[];
87
+ preload: boolean;
88
+ styles: string[];
89
+ subsets?: string[];
90
+ variable?: string;
91
+ weights: string[];
92
+ }
93
+
94
+ interface NormalizedLocalSource {
95
+ file: string;
96
+ style?: string;
97
+ weight?: string;
98
+ }
99
+
100
+ const fontStorageKey = Symbol.for('pnext.compat.font.storage');
101
+ const fontRuntimeGlobal = globalThis as typeof globalThis & Record<symbol, AsyncLocalStorage<FontScope> | undefined>;
102
+ const fontStorage = fontRuntimeGlobal[fontStorageKey] ??= new AsyncLocalStorage<FontScope>();
103
+ const allowedDisplayValues = new Set(['auto', 'block', 'swap', 'fallback', 'optional']);
104
+ const require = createRequire(import.meta.url);
105
+ const googleCssCache = new Map<string, Promise<string>>();
106
+ const googleFontFileCache = new Map<string, Promise<Uint8Array>>();
107
+ const resolvedFontCache = new Map<string, Promise<ResolvedFont>>();
108
+ let googleMetadataCache: Record<string, GoogleFontMetadata> | undefined;
109
+
110
+ /**
111
+ * Every font definition constructed so far, whether or not a render has read its `.className` yet.
112
+ * Definitions are created when the app's font module evaluates - well before the render that registers
113
+ * them - which is what makes the resolve prewarmable.
114
+ */
115
+ const declaredFonts = new Map<string, FontDefinition>();
116
+
117
+ /**
118
+ * Resolve the declared fonts ahead of the render that will ask for them. Fire-and-forget: this only
119
+ * fills `resolvedFontCache`, so a failure costs nothing - the render's own `resolveFont` re-runs and
120
+ * surfaces the error.
121
+ */
122
+ export async function prewarmFonts(
123
+ config: ResolvedConfig,
124
+ options: { dev?: boolean } = {},
125
+ ): Promise<void> {
126
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
127
+ if (process.env.PNEXT_PREWARM_FONTS === '0' || declaredFonts.size === 0) return;
128
+ await Promise.allSettled(
129
+ [...declaredFonts.values()].map(font => resolveFont(font, { config, dev: options.dev })),
130
+ );
131
+ }
132
+
133
+ export function runWithFontScope<T>(callback: () => T): T {
134
+ // Ensure the `Link`-header finalizer is registered before any render runs.
135
+ // Idempotent; register-font also calls it (canonical orchestrator seam).
136
+ registerFontFinalizerOnce();
137
+ return fontStorage.run({ fonts: new Map() }, callback);
138
+ }
139
+
140
+ /** Work-unit stash key for the font `Link` header preloads collected in a render. */
141
+ const FONT_HEADER_PRELOADS = Symbol.for('pnext.compat.font.headerPreloads');
142
+
143
+ export async function fontAssets(config: ResolvedConfig, options: { dev?: boolean } = {}): Promise<ResolvedFontAssets> {
144
+ const scope = fontStorage.getStore();
145
+ if (!scope || scope.fonts.size === 0) return { css: '', preloads: [] };
146
+
147
+ const resolved = await Promise.all(
148
+ [...scope.fonts.values()].map(font => resolveFont(font, { config, dev: options.dev })),
149
+ );
150
+
151
+ const preloads = dedupePreloads(resolved.flatMap(font => font.preloads));
152
+ // Next delivers font preloads via a `Link` response header (not HTML link
153
+ // tags). Stash them on the request work unit; the response finalizer
154
+ // (registerFontResponseFinalizer) reads them back and sets the header.
155
+ stashHeaderPreloads(preloads);
156
+
157
+ const usesFonts = resolved.some(font => font.usesFonts);
158
+ return {
159
+ css: resolved.map(font => font.css).filter(Boolean).join('\n'),
160
+ // Prod matches Next: font preloads travel EXCLUSIVELY in the `Link` response header, so the SSR
161
+ // document carries no `<link as="font">` tags. The live DOM still gets the links post-load via
162
+ // fontPreloadDomScript, which derives them from the emitted @font-face CSS - Next's client inserts
163
+ // them the same way, and the navigation suite counts them in the browser. Dev keeps the document
164
+ // links, since it has no header path.
165
+ preloads:
166
+ preloads.length > 0
167
+ ? options.dev
168
+ ? preloads
169
+ : []
170
+ : usesFonts
171
+ ? [fontPreconnect()]
172
+ : [],
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Inline script that mirrors Next's client-side font preload insertion: scan
178
+ * same-origin stylesheets for @font-face sources whose filename carries the
179
+ * `.p.` preload marker and insert a `<link rel="preload" as="font">` for each
180
+ * (deduped against any existing link). Prod-only companion to the header-only
181
+ * delivery above.
182
+ */
183
+ export function fontPreloadDomScript(): string {
184
+ return (
185
+ '<script>(function(){try{function a(){var seen={},ls=document.querySelectorAll(\'link[as="font"]\');' +
186
+ 'for(var i=0;i<ls.length;i++)seen[ls[i].href]=1;' +
187
+ 'for(var i=0;i<document.styleSheets.length;i++){var rules;try{rules=document.styleSheets[i].cssRules}catch(e){continue}if(!rules)continue;' +
188
+ 'for(var j=0;j<rules.length;j++){var r=rules[j];if(!(r.type===5))continue;' +
189
+ 'var m=/url\\((?:"|\')?([^"\')\\s]*\\.p\\.woff2)/.exec(r.cssText);if(!m)continue;' +
190
+ 'var u;try{u=new URL(m[1],location.href).href}catch(e){continue}if(seen[u])continue;seen[u]=1;' +
191
+ 'var l=document.createElement("link");l.rel="preload";l.as="font";l.type="font/woff2";l.setAttribute("crossorigin","");l.href=u;document.head.appendChild(l)}}}' +
192
+ 'document.readyState==="loading"?document.addEventListener("DOMContentLoaded",a):a()}catch(e){}})();</script>'
193
+ );
194
+ }
195
+
196
+ function stashHeaderPreloads(preloads: MetadataLink[]): void {
197
+ if (preloads.length === 0) return;
198
+ const unit = getWorkUnit();
199
+ if (!unit) return;
200
+ const compat = (unit.compat ??= {});
201
+ const existing = (compat[FONT_HEADER_PRELOADS] as MetadataLink[] | undefined) ?? [];
202
+ compat[FONT_HEADER_PRELOADS] = dedupePreloads([...existing, ...preloads]);
203
+ }
204
+
205
+ /** Read the font `Link` header preloads stashed during the current render. */
206
+ export function takeFontHeaderPreloads(): MetadataLink[] {
207
+ const unit = getWorkUnit();
208
+ const compat = unit?.compat;
209
+ if (!compat) return [];
210
+ const preloads = (compat[FONT_HEADER_PRELOADS] as MetadataLink[] | undefined) ?? [];
211
+ compat[FONT_HEADER_PRELOADS] = undefined;
212
+ return preloads;
213
+ }
214
+
215
+ let fontFinalizerRegistered = false;
216
+ // Host-scoped guard: the finalizer lives on the host, so a fresh host needs it again.
217
+ onExtensionHostReset(() => (fontFinalizerRegistered = false));
218
+
219
+ /**
220
+ * Register the response finalizer that flushes the stashed font preloads to the
221
+ * `Link` header. Idempotent: safe to call from both register-font (the canonical
222
+ * orchestrator seam) and lazily from font-scope setup, so preloads reach the
223
+ * header even before the orchestrator wires register-font in.
224
+ */
225
+ export function registerFontFinalizerOnce(): void {
226
+ if (fontFinalizerRegistered) return;
227
+ fontFinalizerRegistered = true;
228
+ registerResponseFinalizers(ctx => {
229
+ const preloads = takeFontHeaderPreloads();
230
+ if (preloads.length === 0) return;
231
+ const value = preloads.map(fontLinkHeaderValue).join(', ');
232
+ const existing = ctx.headers.get('link');
233
+ ctx.headers.set('link', existing ? `${existing}, ${value}` : value);
234
+ });
235
+ }
236
+
237
+ /** `<url>; rel=preload; as=font; type=font/woff2; crossorigin=""` */
238
+ export function fontLinkHeaderValue(link: MetadataLink): string {
239
+ const parts = [`<${link.url}>`, `rel=${link.rel}`];
240
+ if (link.as) parts.push(`as=${link.as}`);
241
+ if (link.type) parts.push(`type=${link.type}`);
242
+ parts.push('crossorigin=""');
243
+ return parts.join('; ');
244
+ }
245
+
246
+ export function googleFont(family: string) {
247
+ return (options: GoogleFontOptions = {}): NextFontWithVariable => {
248
+ const naming = googleFontNaming(family, options);
249
+ const { className, variable, fontFamily, hash, style, hasVariable } = naming;
250
+ const definition: FontDefinition = {
251
+ className,
252
+ family: fontFamily,
253
+ key: `google:${stableJson({ family, options })}`,
254
+ style,
255
+ variable,
256
+ hasVariable,
257
+ resolve: context => resolveGoogleFont(context, {
258
+ className,
259
+ family,
260
+ fontFamily,
261
+ hash,
262
+ options,
263
+ variable,
264
+ }),
265
+ };
266
+
267
+ return fontObject(definition);
268
+ };
269
+ }
270
+
271
+ export interface LocalFontLoader {
272
+ (options: LocalFontOptions): NextFontWithVariable;
273
+ withFile: typeof localFontWithFile;
274
+ }
275
+
276
+ export const localFont: LocalFontLoader = Object.assign(
277
+ (options: LocalFontOptions) => localFontWithFile(inferLocalFontCallerFile())(options),
278
+ { withFile: localFontWithFile },
279
+ );
280
+
281
+ export function localFontWithFile(callerFile: string | undefined, family?: string) {
282
+ return (options: LocalFontOptions): NextFontWithVariable => {
283
+ const naming = localFontNaming(callerFile, options);
284
+ if (family) {
285
+ naming.fontFamily = family;
286
+ naming.style = fontStyle(family, options, options.fallback ?? []);
287
+ }
288
+ const { className, variable, fontFamily, hash, style, hasVariable } = naming;
289
+ const definition: FontDefinition = {
290
+ className,
291
+ family: fontFamily,
292
+ key: `local:${stableJson({ callerFile, options })}`,
293
+ style,
294
+ variable,
295
+ hasVariable,
296
+ resolve: context => resolveLocalFont(context, {
297
+ callerFile,
298
+ className,
299
+ fontFamily,
300
+ hash,
301
+ options,
302
+ variable,
303
+ }),
304
+ };
305
+
306
+ return fontObject(definition);
307
+ };
308
+ }
309
+
310
+ function fontObject(definition: FontDefinition): NextFontWithVariable {
311
+ // Recorded at construction, not at first `.className` read: the whole point
312
+ // is to know the font exists before any render registers it.
313
+ declaredFonts.set(definition.key, definition);
314
+ // `variable` is exposed only when the loader call passed a `variable` option
315
+ // (matches Next; an extra key would break `toEqual` on the font object).
316
+ const descriptors: PropertyDescriptorMap = {
317
+ className: {
318
+ enumerable: true,
319
+ get() {
320
+ registerFont(definition);
321
+ return definition.className;
322
+ },
323
+ },
324
+ style: {
325
+ enumerable: true,
326
+ get() {
327
+ registerFont(definition);
328
+ return definition.style;
329
+ },
330
+ },
331
+ };
332
+ if (definition.hasVariable) {
333
+ descriptors.variable = {
334
+ enumerable: true,
335
+ get() {
336
+ registerFont(definition);
337
+ return definition.variable;
338
+ },
339
+ };
340
+ }
341
+ return Object.defineProperties({}, descriptors) as NextFontWithVariable;
342
+ }
343
+
344
+ function registerFont(definition: FontDefinition) {
345
+ fontStorage.getStore()?.fonts.set(definition.key, definition);
346
+ }
347
+
348
+ async function resolveFont(definition: FontDefinition, context: FontResolveContext) {
349
+ const key = `${context.config.outPath}:${context.dev ? 'dev' : 'build'}:${definition.key}`;
350
+ let resolved = resolvedFontCache.get(key);
351
+ if (!resolved) {
352
+ resolved = resolveThroughCache(definition, context, key);
353
+ resolvedFontCache.set(key, resolved);
354
+ }
355
+ const font = await resolved;
356
+ // A `pnext build` wiping the output directory under a running server must
357
+ // not leave cached font URLs pointing at deleted files; re-emit them.
358
+ // Font bytes stay cached in memory, so this never refetches the network.
359
+ if (font.files.every(file => existsSync(file))) return font;
360
+ const fresh = resolveAndPersist(definition, context, key);
361
+ resolvedFontCache.set(key, fresh);
362
+ return fresh;
363
+ }
364
+
365
+ /**
366
+ * Dev reuses the persisted resolution when it still describes disk: a restart otherwise re-fetches every
367
+ * Google stylesheet and font file to re-derive URLs whose bytes are already emitted. A build always
368
+ * resolves for real - it owns the output directory it just emptied.
369
+ */
370
+ async function resolveThroughCache(
371
+ definition: FontDefinition,
372
+ context: FontResolveContext,
373
+ key: string,
374
+ ): Promise<ResolvedFont> {
375
+ if (!context.dev) return definition.resolve(context);
376
+ const persisted = await persistedFont(context.config, key);
377
+ if (persisted) return persisted;
378
+ return resolveAndPersist(definition, context, key);
379
+ }
380
+
381
+ function resolveAndPersist(
382
+ definition: FontDefinition,
383
+ context: FontResolveContext,
384
+ key: string,
385
+ ): Promise<ResolvedFont> {
386
+ const resolved = definition.resolve(context);
387
+ if (context.dev) {
388
+ void resolved.then(font => persistFont(context.config, key, font)).catch(() => undefined);
389
+ }
390
+ return resolved;
391
+ }
392
+
393
+ async function resolveGoogleFont(
394
+ context: FontResolveContext,
395
+ request: {
396
+ className: string;
397
+ family: string;
398
+ fontFamily: string;
399
+ hash: string;
400
+ options: GoogleFontOptions;
401
+ variable: string;
402
+ },
403
+ ): Promise<ResolvedFont> {
404
+ const metadata = googleMetadata()[request.family];
405
+ if (!metadata) throw new Error(`Unknown font \`${request.family}\``);
406
+ const options = normalizeGoogleOptions(request.family, metadata, request.options);
407
+ const adjustFontFallback = options.adjustFontFallback;
408
+ const url = googleFontsUrl(request.family, googleFontAxes(metadata, options), options.display);
409
+ const css = (await googleCss(url, request.family, Boolean(context.dev))).split('body {', 1)[0] ?? '';
410
+ const fontFiles = findGoogleFontFiles(css, options.preload ? options.subsets : undefined);
411
+ const emitted = await Promise.all(
412
+ fontFiles.map(async fontFile => ({
413
+ ...fontFile,
414
+ emitted: await emitRemoteFontFile(context, fontFile.googleFontFileUrl, {
415
+ preload: fontFile.preloadFontFile,
416
+ adjustFontFallback,
417
+ }),
418
+ })),
419
+ );
420
+
421
+ let rewrittenCss = css;
422
+ for (const item of emitted) {
423
+ rewrittenCss = rewrittenCss.replaceAll(item.googleFontFileUrl, item.emitted.url);
424
+ }
425
+
426
+ const stack = familyStack(request.fontFamily, options.fallback, adjustFontFallback);
427
+ const scopedCss = [
428
+ rewriteFontFamily(rewrittenCss, request.family, request.fontFamily),
429
+ adjustFontFallback ? fallbackFaceCss(request.fontFamily) : '',
430
+ classRule(request.className, stack, singleCssValue(options.weights), singleCssValue(options.styles)),
431
+ request.options.variable ? variableRule(request.variable, customProperty(request.options.variable), stack) : '',
432
+ ].filter(Boolean).join('\n');
433
+
434
+ return {
435
+ usesFonts: emitted.length > 0,
436
+ css: scopedCss,
437
+ preloads: emitted
438
+ .filter(item => item.preloadFontFile)
439
+ .map(item => fontPreload(item.emitted.url, item.emitted.type)),
440
+ files: emitted.map(item => item.emitted.file),
441
+ };
442
+ }
443
+
444
+ async function resolveLocalFont(
445
+ context: FontResolveContext,
446
+ request: {
447
+ callerFile: string | undefined;
448
+ className: string;
449
+ fontFamily: string;
450
+ hash: string;
451
+ options: LocalFontOptions;
452
+ variable: string;
453
+ },
454
+ ): Promise<ResolvedFont> {
455
+ const display = validateDisplay(request.options.display ?? 'swap');
456
+ const adjustFontFallback = request.options.declarations?.some(item => item.prop === 'font-family')
457
+ ? false
458
+ : true;
459
+ const preload = request.options.preload !== false;
460
+ const sources = normalizeLocalSources(request.options, request.callerFile);
461
+ const emitted = await Promise.all(sources.map(async source => ({
462
+ ...source,
463
+ emitted: await emitLocalFontFile(context, source.file, { preload, adjustFontFallback }),
464
+ })));
465
+ const stack = familyStack(request.fontFamily, request.options.fallback ?? [], adjustFontFallback);
466
+ const declarations = request.options.declarations ?? [];
467
+ const faceCss = emitted.map(source =>
468
+ [
469
+ '@font-face{',
470
+ `font-family:${singleQuoteFamily(request.fontFamily)};`,
471
+ `src:url("${source.emitted.url}") format("${fontFormat(source.emitted.ext)}");`,
472
+ `font-display:${display};`,
473
+ source.weight ?? request.options.weight ? `font-weight:${cssIdentifier(source.weight ?? request.options.weight ?? '')};` : '',
474
+ source.style ?? request.options.style ? `font-style:${cssIdentifier(source.style ?? request.options.style ?? '')};` : '',
475
+ ...declarations.map(item => `${cssProperty(item.prop)}:${cssDeclarationValue(item.value)};`),
476
+ '}',
477
+ ].filter(Boolean).join(''),
478
+ );
479
+
480
+ return {
481
+ usesFonts: emitted.length > 0,
482
+ css: [
483
+ ...faceCss,
484
+ adjustFontFallback ? fallbackFaceCss(request.fontFamily) : '',
485
+ classRule(request.className, stack, request.options.weight, request.options.style),
486
+ request.options.variable ? variableRule(request.variable, customProperty(request.options.variable), stack) : '',
487
+ ].filter(Boolean).join('\n'),
488
+ preloads: preload
489
+ ? emitted.map(item => fontPreload(item.emitted.url, item.emitted.type))
490
+ : [],
491
+ files: emitted.map(item => item.emitted.file),
492
+ sources: emitted.map(source => source.file),
493
+ };
494
+ }
495
+
496
+ function normalizeGoogleOptions(
497
+ family: string,
498
+ metadata: GoogleFontMetadata,
499
+ options: GoogleFontOptions,
500
+ ): NormalizedGoogleOptions {
501
+ const display = validateDisplay(options.display ?? 'swap');
502
+ const preload = options.preload ?? true;
503
+ const subsets = options.subsets;
504
+ if (metadata.subsets.length === 0) {
505
+ // Matches Next: fonts without preloadable subsets silently disable preload.
506
+ } else if (preload) {
507
+ if (!subsets) {
508
+ throw new Error(
509
+ `Preload is enabled but no subsets were specified for font \`${family}\`. ` +
510
+ `Please specify subsets or disable preloading.`,
511
+ );
512
+ }
513
+ for (const subset of subsets) {
514
+ if (!metadata.subsets.includes(subset)) {
515
+ throw new Error(`Unknown subset \`${subset}\` for font \`${family}\`.`);
516
+ }
517
+ }
518
+ }
519
+
520
+ const weights = uniqueArray(options.weight ? arrayValue(options.weight) : []);
521
+ if (weights.length === 0) {
522
+ if (metadata.weights.includes('variable')) weights.push('variable');
523
+ else throw new Error(`Missing weight for font \`${family}\`.`);
524
+ }
525
+ if (weights.length > 1 && weights.includes('variable')) {
526
+ throw new Error(`Unexpected \`variable\` in weight array for font \`${family}\`.`);
527
+ }
528
+ for (const weight of weights) {
529
+ if (!metadata.weights.includes(weight)) throw new Error(`Unknown weight \`${weight}\` for font \`${family}\`.`);
530
+ }
531
+
532
+ const styles = uniqueArray(options.style ? arrayValue(options.style) : []);
533
+ if (styles.length === 0) styles.push(metadata.styles.length === 1 ? (metadata.styles[0] ?? 'normal') : 'normal');
534
+ for (const style of styles) {
535
+ if (!metadata.styles.includes(style)) throw new Error(`Unknown style \`${style}\` for font \`${family}\`.`);
536
+ }
537
+
538
+ if (options.axes) {
539
+ if (!metadata.weights.includes('variable') || weights[0] !== 'variable') {
540
+ throw new Error('Axes can only be defined for variable fonts.');
541
+ }
542
+ const definableAxes = (metadata.axes ?? []).map(axis => axis.tag).filter(tag => tag !== 'wght');
543
+ for (const axis of options.axes) {
544
+ if (!definableAxes.includes(axis)) throw new Error(`Invalid axes value \`${axis}\` for font \`${family}\`.`);
545
+ }
546
+ }
547
+
548
+ return {
549
+ axes: options.axes,
550
+ adjustFontFallback: options.adjustFontFallback ?? true,
551
+ display,
552
+ fallback: options.fallback ?? [],
553
+ preload: metadata.subsets.length === 0 ? false : preload,
554
+ styles,
555
+ subsets,
556
+ variable: options.variable,
557
+ weights,
558
+ };
559
+ }
560
+
561
+ function googleFontAxes(metadata: GoogleFontMetadata, options: NormalizedGoogleOptions) {
562
+ const hasItalic = options.styles.includes('italic');
563
+ const hasNormal = options.styles.includes('normal');
564
+ const ital = hasItalic ? [...(hasNormal ? ['0'] : []), '1'] : undefined;
565
+
566
+ if (options.weights[0] !== 'variable') {
567
+ return { ital, wght: options.weights, variableAxes: undefined as [string, string][] | undefined };
568
+ }
569
+
570
+ let weightAxis: string | undefined;
571
+ const variableAxes: [string, string][] = [];
572
+ for (const axis of metadata.axes ?? []) {
573
+ if (axis.tag === 'wght') weightAxis = `${axis.min}..${axis.max}`;
574
+ else if (options.axes?.includes(axis.tag)) variableAxes.push([axis.tag, `${axis.min}..${axis.max}`]);
575
+ }
576
+
577
+ return {
578
+ ital,
579
+ wght: weightAxis ? [weightAxis] : undefined,
580
+ variableAxes: variableAxes.length > 0 ? variableAxes : undefined,
581
+ };
582
+ }
583
+
584
+ function googleFontsUrl(
585
+ family: string,
586
+ axes: { ital?: string[]; wght?: string[]; variableAxes?: [string, string][] },
587
+ display: FontDisplay,
588
+ ) {
589
+ const variants: [string, string][][] = [];
590
+ if (axes.wght) {
591
+ for (const wght of axes.wght) {
592
+ if (!axes.ital) {
593
+ variants.push([['wght', wght], ...(axes.variableAxes ?? [])]);
594
+ } else {
595
+ for (const ital of axes.ital) variants.push([['ital', ital], ['wght', wght], ...(axes.variableAxes ?? [])]);
596
+ }
597
+ }
598
+ } else if (axes.variableAxes) {
599
+ variants.push([...axes.variableAxes]);
600
+ }
601
+
602
+ if (axes.variableAxes) {
603
+ for (const variant of variants) {
604
+ variant.sort(([a], [b]) => {
605
+ const aLower = a.charCodeAt(0) > 96;
606
+ const bLower = b.charCodeAt(0) > 96;
607
+ if (aLower && !bLower) return -1;
608
+ if (bLower && !aLower) return 1;
609
+ return a > b ? 1 : -1;
610
+ });
611
+ }
612
+ }
613
+
614
+ let url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, '+')}`;
615
+ const firstVariant = variants[0];
616
+ if (firstVariant) {
617
+ url += `:${firstVariant.map(([key]) => key).join(',')}@${variants
618
+ .map(variant => variant.map(([, value]) => value).join(','))
619
+ .sort(sortFontVariantValues)
620
+ .join(';')}`;
621
+ }
622
+ return `${url}&display=${display}`;
623
+ }
624
+
625
+ async function googleCss(url: string, family: string, dev: boolean) {
626
+ let cached = googleCssCache.get(url);
627
+ if (!cached) {
628
+ cached = fetchGoogleCss(url, family, dev);
629
+ googleCssCache.set(url, cached);
630
+ }
631
+ return cached;
632
+ }
633
+
634
+ async function fetchGoogleCss(url: string, family: string, dev: boolean) {
635
+ const mocked = mockedGoogleResponse(url);
636
+ if (typeof mocked === 'string') return mocked;
637
+
638
+ const response = await fetch(url, {
639
+ headers: {
640
+ // Google returns modern woff2 CSS for current browsers.
641
+ 'user-agent': 'Mozilla/5.0 AppleWebKit/537.36 Chrome/120 Safari/537.36',
642
+ },
643
+ });
644
+ if (!response.ok) {
645
+ const message = `Failed to fetch font \`${family}\` from Google Fonts: ${response.status} ${response.statusText}`;
646
+ throw new Error(dev ? `${message}. Use NEXT_FONT_GOOGLE_MOCKED_RESPONSES for offline tests.` : message);
647
+ }
648
+ return response.text();
649
+ }
650
+
651
+ function findGoogleFontFiles(css: string, subsetsToPreload: string[] | undefined) {
652
+ const fontFiles: { googleFontFileUrl: string; preloadFontFile: boolean }[] = [];
653
+ let currentSubset = '';
654
+ for (const line of css.split('\n')) {
655
+ const subset = /\/\* (.+?) \*\//.exec(line)?.[1];
656
+ if (subset) {
657
+ currentSubset = subset;
658
+ continue;
659
+ }
660
+
661
+ const googleFontFileUrl = /src: url\((.+?)\)/.exec(line)?.[1];
662
+ if (!googleFontFileUrl || fontFiles.some(file => file.googleFontFileUrl === googleFontFileUrl)) continue;
663
+ fontFiles.push({
664
+ googleFontFileUrl,
665
+ preloadFontFile: Boolean(subsetsToPreload?.includes(currentSubset)),
666
+ });
667
+ }
668
+ return fontFiles;
669
+ }
670
+
671
+ interface EmitFontOptions {
672
+ preload: boolean;
673
+ adjustFontFallback: boolean;
674
+ }
675
+
676
+ async function emitRemoteFontFile(context: FontResolveContext, url: string, emit: EmitFontOptions) {
677
+ let cached = googleFontFileCache.get(url);
678
+ if (!cached) {
679
+ cached = fetchFontFile(url);
680
+ googleFontFileCache.set(url, cached);
681
+ }
682
+ const bytes = await cached;
683
+ return emitFontBytes(context, bytes, fontExt(url), emit);
684
+ }
685
+
686
+ async function fetchFontFile(url: string) {
687
+ const mocked = mockedGoogleResponse(url);
688
+ if (mocked instanceof Uint8Array) return mocked;
689
+ if (typeof mocked === 'string') return new TextEncoder().encode(mocked);
690
+
691
+ const response = await fetch(url);
692
+ if (!response.ok) throw new Error(`Failed to fetch font file from \`${url}\`.`);
693
+ return new Uint8Array(await response.arrayBuffer());
694
+ }
695
+
696
+ async function emitLocalFontFile(context: FontResolveContext, file: string, emit: EmitFontOptions) {
697
+ const bytes = await readFile(file);
698
+ return emitFontBytes(context, bytes, fontExt(file), emit, file);
699
+ }
700
+
701
+ async function emitFontBytes(
702
+ context: FontResolveContext,
703
+ bytes: Uint8Array,
704
+ ext: string,
705
+ emit: EmitFontOptions,
706
+ sourceFile?: string,
707
+ ) {
708
+ const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16);
709
+ // Next's loader names emitted files `[hash]-s.p.[ext]`: `-s` when a
710
+ // size-adjust fallback font is used, `.p` when the font is preloaded.
711
+ const filename = `${hash}${emit.adjustFontFallback ? '-s' : ''}${emit.preload ? '.p' : ''}.${ext}`;
712
+ // Served from `/_next/static/media/` (files under public/ map 1:1 to the URL
713
+ // path). Dev stashes under cache/ but keeps the same relative layout.
714
+ const mediaSegments = ['_next', 'static', 'media'];
715
+ const outRoot = context.dev
716
+ ? path.join(context.config.outPath, 'cache')
717
+ : path.join(context.config.outPath, 'public');
718
+ const outDir = path.join(outRoot, ...mediaSegments);
719
+ const file = path.join(outDir, filename);
720
+ await mkdir(outDir, { recursive: true });
721
+ if (!existsSync(file)) {
722
+ if (sourceFile) await copyFile(sourceFile, file);
723
+ else await writeFile(file, bytes);
724
+ }
725
+ return { ext, file, type: fontMimeType(ext), url: `/${mediaSegments.join('/')}/${filename}` };
726
+ }
727
+
728
+ function normalizeLocalSources(options: LocalFontOptions, callerFile: string | undefined): NormalizedLocalSource[] {
729
+ if (!callerFile) throw new Error('next/font/local requires PNext server source transforms.');
730
+ const values = Array.isArray(options.src) ? options.src : [options.src];
731
+ return values.map(value => {
732
+ const item = typeof value === 'string' ? { path: value } : value;
733
+ if (!item.path) throw new Error('next/font/local src entries require a path.');
734
+ const file = path.resolve(path.dirname(callerFile), item.path);
735
+ if (!existsSync(file)) throw new Error(`Font file not found: ${item.path}`);
736
+ return { file, style: item.style, weight: item.weight };
737
+ });
738
+ }
739
+
740
+ function inferLocalFontCallerFile(): string | undefined {
741
+ const stack = new Error().stack;
742
+ if (!stack) return undefined;
743
+ for (const line of stack.split('\n')) {
744
+ const file = stackFrameFile(line);
745
+ if (!file) continue;
746
+ if (file.replace(/\\/g, '/').includes('/src/compat/next/font/')) continue;
747
+ return originalSourceFile(file);
748
+ }
749
+ return undefined;
750
+ }
751
+
752
+ function stackFrameFile(line: string) {
753
+ const match = /\(?((?:file:\/\/)?\/[^():]+\.[cm]?[jt]sx?|(?:file:\/\/)?\/[^():]+\.mjs):\d+:\d+\)?/.exec(line);
754
+ const file = match?.[1];
755
+ if (!file) return undefined;
756
+ return file.startsWith('file://') ? fileURLToPath(file) : file;
757
+ }
758
+
759
+ function originalSourceFile(file: string) {
760
+ const source = sourceComment(file);
761
+ if (!source) return file;
762
+ const normalized = source.replace(/\\/g, '/');
763
+ if (normalized.startsWith('file://')) return fileURLToPath(normalized);
764
+ if (path.isAbsolute(normalized)) return existsSync(normalized) ? normalized : file;
765
+ const nodeModulesIndex = normalized.indexOf('node_modules/');
766
+ const relative = nodeModulesIndex === -1 ? normalized : normalized.slice(nodeModulesIndex);
767
+ const candidate = path.resolve(compiledProjectRoot(file), relative);
768
+ return existsSync(candidate) ? candidate : file;
769
+ }
770
+
771
+ function sourceComment(file: string) {
772
+ if (!existsSync(file)) return undefined;
773
+ const firstLine = readFileSync(file, 'utf8').split(/\r?\n/, 1)[0]?.trim();
774
+ return /^\/\/\s+(.+)$/.exec(firstLine ?? '')?.[1];
775
+ }
776
+
777
+ function compiledProjectRoot(file: string) {
778
+ const marker = `${path.sep}.pnext${path.sep}`;
779
+ const index = file.indexOf(marker);
780
+ return index === -1 ? process.cwd() : file.slice(0, index);
781
+ }
782
+
783
+ function googleMetadata() {
784
+ if (!googleMetadataCache) {
785
+ googleMetadataCache = loadGoogleMetadata();
786
+ }
787
+ return googleMetadataCache;
788
+ }
789
+
790
+ function loadGoogleMetadata(): Record<string, GoogleFontMetadata> {
791
+ const override = process.env.PNEXT_FONT_GOOGLE_METADATA;
792
+ if (override) {
793
+ const metadata = readGoogleMetadataFile(override);
794
+ if (metadata) return metadata;
795
+ throw new Error(`Invalid Google font metadata file: ${override}`);
796
+ }
797
+
798
+ const metadata =
799
+ tryLoadGoogleMetadata('next-font/google/font-data.json') ??
800
+ tryLoadGoogleMetadata('next-font');
801
+ if (metadata) return metadata;
802
+
803
+ throw new Error(
804
+ 'next/font/google requires Google font metadata. Install `next-font`.',
805
+ );
806
+ }
807
+
808
+ function tryLoadGoogleMetadata(specifier: string): Record<string, GoogleFontMetadata> | undefined {
809
+ try {
810
+ const loaded = require(specifier) as {
811
+ default?: unknown;
812
+ googleFontsMetadata?: unknown;
813
+ } | Record<string, GoogleFontMetadata>;
814
+ const metadata =
815
+ 'googleFontsMetadata' in loaded
816
+ ? loaded.googleFontsMetadata
817
+ : 'default' in loaded
818
+ ? loaded.default
819
+ : loaded;
820
+ return isGoogleMetadata(metadata) ? metadata : undefined;
821
+ } catch {
822
+ return undefined;
823
+ }
824
+ }
825
+
826
+ function readGoogleMetadataFile(file: string): Record<string, GoogleFontMetadata> | undefined {
827
+ const metadata: unknown = JSON.parse(readFileSync(file, 'utf8'));
828
+ return isGoogleMetadata(metadata) ? metadata : undefined;
829
+ }
830
+
831
+ function isGoogleMetadata(value: unknown): value is Record<string, GoogleFontMetadata> {
832
+ if (!value || typeof value !== 'object') return false;
833
+ const inter = (value as Record<string, GoogleFontMetadata>).Inter;
834
+ return Boolean(
835
+ inter &&
836
+ Array.isArray(inter.weights) &&
837
+ Array.isArray(inter.styles) &&
838
+ Array.isArray(inter.subsets),
839
+ );
840
+ }
841
+
842
+ function mockedGoogleResponse(url: string) {
843
+ const mockFile = process.env.NEXT_FONT_GOOGLE_MOCKED_RESPONSES;
844
+ if (!mockFile) return undefined;
845
+ const mocked = require(mockFile) as Record<string, string>;
846
+ const value = mocked[url];
847
+ if (value === undefined) throw new Error(`Missing mocked response for URL: ${url}`);
848
+ if (typeof value === 'string' && value.startsWith('/') && existsSync(value)) {
849
+ return new Uint8Array(readFileSync(value));
850
+ }
851
+ return value;
852
+ }
853
+
854
+ function rewriteFontFamily(css: string, originalFamily: string, scopedFamily: string) {
855
+ const familyPattern = escapeRegex(originalFamily);
856
+ return css.replace(
857
+ new RegExp(`font-family:\\s*(["']?)${familyPattern}\\1`, 'g'),
858
+ `font-family:${singleQuoteFamily(scopedFamily)}`,
859
+ );
860
+ }
861
+
862
+ /** Metrics-adjusted fallback `@font-face` (`'<family> Fallback'`). We omit the
863
+ * ascent/descent/size-adjust metrics (would need fontkit); the local() source
864
+ * never loads, so the browser resolves the real font or the next stack entry. */
865
+ function fallbackFaceCss(family: string) {
866
+ return `@font-face{font-family:${singleQuoteFamily(`${family} Fallback`)};src:local("Arial");}`;
867
+ }
868
+
869
+ function classRule(className: string, familyStack: string, weight?: string, style?: string) {
870
+ return [
871
+ `.${className}{font-family:${familyStack};`,
872
+ weight && !isRangeValue(weight) ? `font-weight:${cssIdentifier(weight)};` : '',
873
+ style && !isRangeValue(style) ? `font-style:${cssIdentifier(style)};` : '',
874
+ '}',
875
+ ].join('');
876
+ }
877
+
878
+ function variableRule(className: string, variable: string, familyStack: string) {
879
+ return `.${className}{${variable}:${familyStack};}`;
880
+ }
881
+
882
+ function fontPreload(url: string, type: string): MetadataLink {
883
+ return { rel: 'preload', url, as: 'font', type, crossOrigin: '' };
884
+ }
885
+
886
+ /** Single `<link rel="preconnect" href="/" crossorigin="">` for the
887
+ * `preload:false` (no preloaded fonts) case. */
888
+ function fontPreconnect(): MetadataLink {
889
+ return { rel: 'preconnect', url: '/', crossOrigin: '' };
890
+ }
891
+
892
+ function dedupePreloads(preloads: MetadataLink[]) {
893
+ const seen = new Set<string>();
894
+ return preloads.filter(preload => {
895
+ if (seen.has(preload.url)) return false;
896
+ seen.add(preload.url);
897
+ return true;
898
+ });
899
+ }
900
+
901
+ function validateDisplay(value: FontDisplay) {
902
+ if (!allowedDisplayValues.has(value)) throw new Error(`Invalid font display value \`${value}\`.`);
903
+ return value;
904
+ }
905
+
906
+ function fontExt(value: string) {
907
+ return /\.(woff2?|eot|ttf|otf)(?:[?#]|$)/.exec(value)?.[1] ?? 'woff2';
908
+ }
909
+
910
+ function fontMimeType(ext: string) {
911
+ if (ext === 'woff') return 'font/woff';
912
+ if (ext === 'ttf') return 'font/ttf';
913
+ if (ext === 'otf') return 'font/otf';
914
+ if (ext === 'eot') return 'application/vnd.ms-fontobject';
915
+ return 'font/woff2';
916
+ }
917
+
918
+ function fontFormat(ext: string) {
919
+ if (ext === 'ttf') return 'truetype';
920
+ if (ext === 'otf') return 'opentype';
921
+ return ext;
922
+ }
923
+
924
+ function singleCssValue(values: string[]) {
925
+ return values.length === 1 ? values[0] : undefined;
926
+ }
927
+
928
+ function isRangeValue(value: string) {
929
+ return value.trim().includes(' ');
930
+ }
931
+
932
+ function arrayValue<T>(value: T | T[]) {
933
+ return Array.isArray(value) ? value : [value];
934
+ }
935
+
936
+ function uniqueArray<T>(items: T[]) {
937
+ return [...new Set(items)];
938
+ }
939
+
940
+
941
+ function customProperty(value: string) {
942
+ if (!/^--[-_a-zA-Z0-9]+$/.test(value)) throw new Error(`Invalid font variable name: ${value}`);
943
+ return value;
944
+ }
945
+
946
+ function cssIdentifier(value: string) {
947
+ if (!/^[-_a-zA-Z0-9. ]+$/.test(value)) throw new Error(`Invalid font option value: ${value}`);
948
+ return value;
949
+ }
950
+
951
+ function cssProperty(value: string) {
952
+ if (!/^[-_a-zA-Z0-9]+$/.test(value)) throw new Error(`Invalid CSS property: ${value}`);
953
+ return value;
954
+ }
955
+
956
+ function cssDeclarationValue(value: string) {
957
+ if (/[;{}]/.test(value)) throw new Error(`Invalid CSS declaration value: ${value}`);
958
+ return value;
959
+ }
960
+
961
+ function sortFontVariantValues(a: string, b: string) {
962
+ if (a.includes(',') && b.includes(',')) {
963
+ const [aPrefix, aSuffix] = a.split(',', 2);
964
+ const [bPrefix, bSuffix] = b.split(',', 2);
965
+ return aPrefix === bPrefix
966
+ ? parseInt(aSuffix ?? '0', 10) - parseInt(bSuffix ?? '0', 10)
967
+ : parseInt(aPrefix ?? '0', 10) - parseInt(bPrefix ?? '0', 10);
968
+ }
969
+ return parseInt(a, 10) - parseInt(b, 10);
970
+ }
971
+
972
+ function escapeRegex(value: string) {
973
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
974
+ }