@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,22 @@
1
+ import type { RouteManifestEntry } from '../types';
2
+
3
+ export function clientEntryName(route: Pick<RouteManifestEntry, 'id'>) {
4
+ // Chunk filenames are content/identity-hashed rather than named after the
5
+ // route (Next parity): an app route name in a JS chunk filename lets a test's
6
+ // Playwright route interceptor (e.g. `**/search*`) accidentally capture the
7
+ // route's own client entry and wedge navigation. Hashing the route id keeps
8
+ // route names out of asset filenames while staying deterministic across
9
+ // processes (build, dev, renderer, and the manifest all derive the name here).
10
+ return route.id === 'index' ? 'pnext-client' : `pnext-client-${routeHash(route.id)}`;
11
+ }
12
+
13
+ // FNV-1a 32-bit — deterministic and dependency-free (no crypto import, so this
14
+ // module stays safe wherever it is bundled). Same route id → same chunk name.
15
+ function routeHash(id: string): string {
16
+ let hash = 0x811c9dc5;
17
+ for (let index = 0; index < id.length; index++) {
18
+ hash ^= id.charCodeAt(index);
19
+ hash = Math.imul(hash, 0x01000193);
20
+ }
21
+ return (hash >>> 0).toString(36);
22
+ }
@@ -0,0 +1,621 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, readdirSync, statSync, type Dirent } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { build, type BuildOptions, type Metafile, type Plugin } from 'esbuild';
5
+ import type { ResolvedConfig } from '../config';
6
+ import { getImportAliasExtensions } from '../extensions';
7
+ import { scanFacts } from '../resolve/scan-facts';
8
+ import { ensureDir, writeText } from '../utils/fs';
9
+ import { escapeRegex } from '../utils/source';
10
+ import { CLIENT_RUNTIME_MODULE } from './entry';
11
+
12
+ /**
13
+ * The route-independent half of every client bundle - pnext's own client runtime, the compat
14
+ * surface and preact - built ONCE into a content-addressed artifact that route entries import
15
+ * instead of re-bundling, so a route entry carries only user code.
16
+ *
17
+ * Two things keep it byte-neutral. The artifact is ONE module aggregating every demanded
18
+ * (specifier, export name) pair under a mangled name, not one esbuild entry point per framework
19
+ * module: entry points keep every export of every module live, and one file per specifier also pays
20
+ * gzip's per-stream dictionary reset. And the aggregate is keyed on the DEMANDED export surface, so
21
+ * esbuild shakes it to the same closure the route build would have.
22
+ */
23
+ export interface PrebuiltRuntime {
24
+ /** Content address of (framework source × alias set × build options). */
25
+ key: string;
26
+ /** Directory holding the artifact. */
27
+ dir: string;
28
+ /** URL prefix route entries import the artifact from — the SERVED url. */
29
+ publicPath: string;
30
+ /**
31
+ * The same directory as an `assets/…` path, for the preload bookkeeping that
32
+ * speaks in app-relative asset names rather than urls.
33
+ */
34
+ assetPath: string;
35
+ /** specifier → every export name that module has (learned by a probe build). */
36
+ exports: Map<string, string[]>;
37
+ /** specifier → the export names route entries actually bind. What ships. */
38
+ surface: Map<string, Set<string>>;
39
+ /** Artifact files statically reachable from the aggregate, relative to it. */
40
+ closure: { static: string[]; dynamic: string[] };
41
+ }
42
+
43
+ /** The aggregate's file name inside the artifact dir. */
44
+ export const PREBUILT_MODULE = 'pnext-runtime.js';
45
+
46
+ /** Framework source root — anything resolving inside it is route-independent. */
47
+ const frameworkSrc = path.resolve(import.meta.dirname, '..');
48
+
49
+ /**
50
+ * The vendor half of the surface. Single-instance modules (context identity,
51
+ * option hooks, error interception), so they must come from the artifact from
52
+ * the very first route build, never bundled per route beside it.
53
+ */
54
+ const vendorSpecifiers = [
55
+ 'preact',
56
+ 'preact/hooks',
57
+ 'preact/compat',
58
+ 'preact/compat/client',
59
+ 'preact/jsx-runtime',
60
+ 'preact/jsx-dev-runtime',
61
+ ];
62
+
63
+ /**
64
+ * The react-compat aliases seed the artifact alongside the vendors: their targets are `export *`
65
+ * facades over preact/compat, so a route build that bundles one instead of prebuilding it demands
66
+ * preact/compat's WHOLE surface. Everything else is discovered lazily - a leaf compat module
67
+ * bundled once costs only what its importer named.
68
+ */
69
+ const reactAliasSpecifier = /^react(?:-dom)?(?:\/|$)/;
70
+
71
+ /**
72
+ * The React Compiler's runtime is resolved per build (and asserted on in the
73
+ * route bundle), so it stays out of the artifact.
74
+ */
75
+ const isCompilerRuntime = (specifier: string) => specifier.endsWith('compiler-runtime');
76
+
77
+ const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
78
+
79
+ /** (specifier, export name) → one collision-free identifier in the aggregate. */
80
+ export function mangledName(specifier: string, name: string) {
81
+ return `__p_${shortHash(specifier)}_${name}`;
82
+ }
83
+
84
+ function shortHash(value: string) {
85
+ return createHash('sha1').update(value).digest('hex').slice(0, 8);
86
+ }
87
+
88
+ function seedSpecifiers(aliases: string[]) {
89
+ const react = aliases.filter(
90
+ alias => reactAliasSpecifier.test(alias) && !isCompilerRuntime(alias),
91
+ );
92
+ return [...new Set([...vendorSpecifiers, ...react])];
93
+ }
94
+
95
+ /** Every framework specifier this app could demand: aliases + preact + pnext src. */
96
+ export function prebuiltSpecifierFilter(config: ResolvedConfig): RegExp {
97
+ const aliases = Object.keys(getImportAliasExtensions().aliases(config, 'client'));
98
+ const bare = [...new Set([...aliases, ...vendorSpecifiers])];
99
+ return new RegExp(`^(?:${bare.map(escapeRegex).join('|')}|${escapeRegex(frameworkSrc)}/)`);
100
+ }
101
+
102
+ /**
103
+ * Does this specifier name route-independent framework code? The generated runtime module is
104
+ * deliberately NOT one: its source is a function of the ClientRuntimeFacts booleans, so prebuilding it
105
+ * would put the facts in the artifact key and give dev one artifact per route shape. It is generated
106
+ * glue whose weight - router, compat, preact - is external through it anyway.
107
+ */
108
+ export function isFrameworkSpecifier(specifier: string, filter: RegExp) {
109
+ if (specifier === CLIENT_RUNTIME_MODULE) return false;
110
+ if (isCompilerRuntime(specifier)) return false;
111
+ if (specifier.startsWith(frameworkSrc)) return true;
112
+ return filter.test(specifier) && !specifier.startsWith('.');
113
+ }
114
+
115
+ /**
116
+ * Content address of the artifact. Not keyed on the *discovered* surface - a route build learns that
117
+ * while it runs, so a surface-dependent url could only be known after the entries that must import it
118
+ * were emitted. It IS keyed on the surface *signature*, which is known before any build: the compat
119
+ * facts the generated entry emits its framework imports from plus the runtime record. Routes sharing
120
+ * that signature share an artifact; routes that do not get their own.
121
+ */
122
+ export function prebuiltRuntimeKey(
123
+ config: ResolvedConfig,
124
+ buildOptions: BuildOptions,
125
+ signature = '',
126
+ ) {
127
+ const hash = createHash('sha1');
128
+ hash.update(frameworkSourceHash());
129
+ hash.update('\0');
130
+ hash.update(JSON.stringify(Object.keys(getImportAliasExtensions().aliases(config, 'client'))));
131
+ hash.update('\0');
132
+ hash.update(
133
+ JSON.stringify([buildOptions.define, buildOptions.minify, buildOptions.target, buildOptions.pure]),
134
+ );
135
+ hash.update('\0');
136
+ hash.update(signature);
137
+ return hash.digest('hex').slice(0, 16);
138
+ }
139
+
140
+ let cachedSourceHash: string | undefined;
141
+
142
+ /**
143
+ * Hash of pnext's own source. Cheap on the cold path: mtime+size per file, not contents - the tree is
144
+ * pnext's own install, so a same-mtime file is the same file.
145
+ */
146
+ function frameworkSourceHash() {
147
+ if (cachedSourceHash) return cachedSourceHash;
148
+ const hash = createHash('sha1');
149
+ const walk = (dir: string) => {
150
+ let entries: Dirent[];
151
+ try {
152
+ entries = readdirSync(dir, { withFileTypes: true });
153
+ } catch {
154
+ return;
155
+ }
156
+ for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
157
+ const file = path.join(dir, entry.name);
158
+ if (entry.isDirectory()) walk(file);
159
+ else if (/\.(?:tsx?|jsx?|mjs|cjs)$/.test(entry.name)) {
160
+ const stats = statSync(file, { throwIfNoEntry: false });
161
+ if (stats) hash.update(`${file}:${stats.size}:${stats.mtimeMs}\n`);
162
+ }
163
+ }
164
+ };
165
+ walk(frameworkSrc);
166
+ cachedSourceHash = hash.digest('hex').slice(0, 16);
167
+ return cachedSourceHash;
168
+ }
169
+
170
+ export interface ArtifactOptions {
171
+ /** The client import aliases in force, part of the key and of the seed. */
172
+ aliases: string[];
173
+ dir: string;
174
+ publicPath: string;
175
+ assetPath: string;
176
+ key: string;
177
+ buildOptions: BuildOptions;
178
+ /** Fresh plugin chain per build — esbuild plugins carry per-build state. */
179
+ plugins: () => Plugin[];
180
+ }
181
+
182
+ /**
183
+ * Learn every export name of each specifier by building throwaway `export *`
184
+ * shims for their metafile alone. This is what lets a route build offer a
185
+ * framework module's whole surface and let esbuild pick the names it needs: an
186
+ * `export *` cannot be enumerated from source text, because the compat modules
187
+ * re-export through several levels of their own stars.
188
+ */
189
+ async function probeExports(specifiers: string[], options: ArtifactOptions) {
190
+ const found = new Map<string, string[]>();
191
+ if (specifiers.length === 0) return found;
192
+ const probeDir = path.join(options.dir, '.probe');
193
+ await ensureDir(probeDir);
194
+ const entryPoints: { in: string; out: string }[] = [];
195
+ for (const specifier of specifiers) {
196
+ const file = path.join(probeDir, `${shortHash(specifier)}.ts`);
197
+ await writeText(file, `export * from ${JSON.stringify(specifier)};\n`);
198
+ entryPoints.push({ in: file, out: shortHash(specifier) });
199
+ }
200
+ const result = await build({
201
+ ...options.buildOptions,
202
+ entryPoints,
203
+ outdir: probeDir,
204
+ entryNames: '[name]',
205
+ splitting: true,
206
+ write: false,
207
+ metafile: true,
208
+ plugins: options.plugins(),
209
+ });
210
+ for (const specifier of specifiers) {
211
+ const output = Object.entries(result.metafile.outputs).find(
212
+ ([name]) => path.basename(name) === `${shortHash(specifier)}.js`,
213
+ );
214
+ // `export *` never carries `default`; probeSurface asks for it separately.
215
+ found.set(specifier, (output?.[1].exports ?? []).filter(name => identifier.test(name)));
216
+ }
217
+ return found;
218
+ }
219
+
220
+ /**
221
+ * The export names of each specifier, VERIFIED against a real aggregate build. Two things need
222
+ * checking: `export *` never carries `default`, so a default has to be asked for separately; and
223
+ * esbuild ACCEPTS `export { default } from "x"` on a module that has none - CJS interop makes it
224
+ * undecidable at parse time - then silently emits nothing, which would leave a route entry importing
225
+ * a name the artifact never provides and kill the page on load. So the candidate surface is built
226
+ * once, unwritten, and every name that did not materialize is dropped.
227
+ */
228
+ async function probeSurface(specifiers: string[], options: ArtifactOptions) {
229
+ const probed = await probeExports(specifiers, options);
230
+ if (probed.size === 0) return probed;
231
+ const candidate = new Map(
232
+ [...probed].map(([specifier, names]) => [specifier, new Set([...names, 'default'])]),
233
+ );
234
+ const { text } = await buildAggregate(options, candidate, false);
235
+ return new Map(
236
+ [...candidate].map(([specifier, names]) => [
237
+ specifier,
238
+ [...names].filter(name => text.includes(mangledName(specifier, name))),
239
+ ]),
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Build the aggregate module for a surface. `write` off keeps it in memory, for
245
+ * the verification pass that must not clobber the artifact on disk.
246
+ */
247
+ async function buildAggregate(
248
+ options: ArtifactOptions,
249
+ surface: Map<string, Set<string>>,
250
+ write: boolean,
251
+ ) {
252
+ const lines: string[] = [];
253
+ for (const specifier of [...surface.keys()].sort()) {
254
+ const names = [...surface.get(specifier)!].filter(name => identifier.test(name)).sort();
255
+ const spec = JSON.stringify(specifier);
256
+ if (names.length === 0) lines.push(`import ${spec};`);
257
+ else {
258
+ const clause = names.map(name => `${name} as ${mangledName(specifier, name)}`).join(', ');
259
+ lines.push(`export { ${clause} } from ${spec};`);
260
+ }
261
+ }
262
+ const aggregateDir = path.join(options.dir, '.aggregate');
263
+ await ensureDir(aggregateDir);
264
+ const entry = path.join(aggregateDir, 'runtime.ts');
265
+ await writeText(entry, `${lines.join('\n')}\n`);
266
+
267
+ const result = await build({
268
+ ...options.buildOptions,
269
+ entryPoints: [{ in: entry, out: PREBUILT_MODULE.replace(/\.js$/, '') }],
270
+ outdir: options.dir,
271
+ entryNames: '[name]',
272
+ splitting: true,
273
+ write,
274
+ metafile: true,
275
+ plugins: options.plugins(),
276
+ });
277
+ const emitted = result.outputFiles?.find(file => path.basename(file.path) === PREBUILT_MODULE);
278
+ const text = emitted?.text ?? readFileSafe(path.join(options.dir, PREBUILT_MODULE));
279
+ return { text, metafile: result.metafile };
280
+ }
281
+
282
+ function readFileSafe(file: string) {
283
+ try {
284
+ return readFileSync(file, 'utf8');
285
+ } catch {
286
+ return '';
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Build and record the artifact: one module re-exporting exactly the demanded
292
+ * (specifier, name) pairs under mangled names, plus whatever chunks its own
293
+ * dynamic imports split into.
294
+ */
295
+ export async function buildPrebuiltRuntime(
296
+ options: ArtifactOptions,
297
+ exportsBySpecifier: Map<string, string[]>,
298
+ surface: Map<string, Set<string>>,
299
+ ): Promise<PrebuiltRuntime> {
300
+ const { metafile } = await buildAggregate(options, surface, true);
301
+ return {
302
+ key: options.key,
303
+ dir: options.dir,
304
+ publicPath: options.publicPath,
305
+ assetPath: options.assetPath,
306
+ exports: exportsBySpecifier,
307
+ surface,
308
+ closure: aggregateClosure(metafile),
309
+ };
310
+ }
311
+
312
+
313
+ function aggregateClosure(metafile: Metafile) {
314
+ const entry = Object.keys(metafile.outputs).find(name => path.basename(name) === PREBUILT_MODULE);
315
+ const statics = new Set<string>();
316
+ const dynamics = new Set<string>();
317
+ if (!entry) return { static: [], dynamic: [] };
318
+ const seen = new Set([`${entry}:static`]);
319
+ const queue = [{ output: entry, dynamic: false }];
320
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
321
+ for (let head = 0; head < queue.length; head += 1) {
322
+ const current = queue[head]!;
323
+ for (const imported of metafile.outputs[current.output]?.imports ?? []) {
324
+ if (imported.kind !== 'import-statement' && imported.kind !== 'dynamic-import') continue;
325
+ const dynamic = current.dynamic || imported.kind === 'dynamic-import';
326
+ const visitKey = `${imported.path}:${dynamic ? 'dynamic' : 'static'}`;
327
+ if (seen.has(visitKey)) continue;
328
+ seen.add(visitKey);
329
+ queue.push({ output: imported.path, dynamic });
330
+ const relative = path.relative(path.dirname(entry), imported.path).split(path.sep).join('/');
331
+ (dynamic ? dynamics : statics).add(relative);
332
+ }
333
+ }
334
+ for (const name of statics) dynamics.delete(name);
335
+ return { static: [...statics], dynamic: [...dynamics] };
336
+ }
337
+ /**
338
+ * Route-build seam. A framework specifier resolves to a virtual module that re-exports the target's
339
+ * whole surface from the artifact url under mangled names; esbuild shakes that down to the names the
340
+ * importer actually uses, so the emitted entry names its own demand and `demandedFromOutputs` reads it
341
+ * straight back off the output. A specifier the artifact has not probed yet is declined and recorded -
342
+ * bundled correctly this once, prebuilt from the next build on.
343
+ */
344
+ export function prebuiltExternalPlugin(
345
+ runtime: PrebuiltRuntime,
346
+ filter: RegExp,
347
+ unprobed: Set<string>,
348
+ sourceOf: (importer: string) => string | undefined,
349
+ ): Plugin {
350
+ const namespace = 'pnext-prebuilt';
351
+ const moduleUrl = prebuiltModuleUrl(runtime);
352
+ return {
353
+ name: 'pnext-prebuilt-runtime',
354
+ setup(build) {
355
+ build.onResolve({ filter: new RegExp(`^${escapeRegex(moduleUrl)}$`) }, () => ({
356
+ path: moduleUrl,
357
+ external: true,
358
+ }));
359
+ build.onResolve({ filter }, args => {
360
+ if (args.namespace === namespace) return undefined;
361
+ if (!isFrameworkSpecifier(args.path, filter)) return undefined;
362
+ if (!runtime.exports.has(args.path)) {
363
+ unprobed.add(args.path);
364
+ return undefined;
365
+ }
366
+ // A dynamic import takes the module as a NAMESPACE - every export is live - so routing one
367
+ // through the artifact would drag the whole surface into the initial tier that the dynamic
368
+ // import exists to stay out of. Leave it deferred, unless the artifact already ships that
369
+ // module, in which case serving it from there keeps the single instance.
370
+ if (args.kind === 'dynamic-import' && !runtime.surface.has(args.path)) return undefined;
371
+ // The importer travels in the path because onLoad is not told it, and
372
+ // the demanded names are a property of the IMPORTER, not the specifier:
373
+ // esbuild cannot shake named imports out of an external module, so
374
+ // whatever the virtual module offers is what ships.
375
+ return { path: `${args.path}\0${args.importer}`, namespace };
376
+ });
377
+ build.onLoad({ filter: /.*/, namespace }, args => {
378
+ const separator = args.path.lastIndexOf('\0');
379
+ const specifier = args.path.slice(0, separator);
380
+ const importer = args.path.slice(separator + 1);
381
+ const available = runtime.exports.get(specifier) ?? [];
382
+ const demanded = demandedNames(importer, specifier, sourceOf(importer), available);
383
+ const clause = demanded
384
+ .map(name => `${mangledName(specifier, name)} as ${name}`)
385
+ .join(', ');
386
+ const contents =
387
+ clause.length > 0
388
+ ? `export { ${clause} } from ${JSON.stringify(moduleUrl)};\n`
389
+ : `import ${JSON.stringify(moduleUrl)};\n`;
390
+ return { contents, loader: 'js', resolveDir: path.dirname(specifier) };
391
+ });
392
+ },
393
+ };
394
+ }
395
+ /**
396
+ * The names one importer binds off one specifier. A namespace import, an `export * from`, or an import
397
+ * no source text names (esbuild synthesizes the automatic JSX runtime one) demands the module's whole
398
+ * surface.
399
+ */
400
+ function demandedNames(
401
+ importer: string,
402
+ specifier: string,
403
+ source: string | undefined,
404
+ available: string[],
405
+ ) {
406
+ if (source === undefined) return available;
407
+ // The generated entries are virtual and their importer id carries no extension; scanFacts picks
408
+ // the parser off the name, so an id like `index` would parse as plain JS, leaving the entry's
409
+ // TypeScript unreadable - which reads as "demands everything".
410
+ const sourceFile = /\.[cm]?[jt]sx?$/i.test(importer) ? importer : `${importer}.tsx`;
411
+ let edges;
412
+ try {
413
+ edges = scanFacts(sourceFile, source).imports;
414
+ } catch {
415
+ return available;
416
+ }
417
+ const names = new Set<string>();
418
+ let named = false;
419
+ for (const edge of edges) {
420
+ if (edge.specifier !== specifier) continue;
421
+ named = true;
422
+ if (edge.star || edge.exports.includes('*')) return available;
423
+ for (const name of edge.exports) names.add(name);
424
+ }
425
+ if (!named) return available;
426
+ const offered = available.filter(name => names.has(name));
427
+ // A name the target does not export is the build's error to report, not
428
+ // something to silently drop — offer it and let esbuild say so.
429
+ for (const name of names) if (!available.includes(name)) offered.push(name);
430
+ return offered;
431
+ }
432
+
433
+ export function prebuiltModuleUrl(runtime: Pick<PrebuiltRuntime, 'publicPath'>) {
434
+ return `${runtime.publicPath}/${PREBUILT_MODULE}`;
435
+ }
436
+ /**
437
+ * Read the surface the build actually demanded back off its own output: every mangled name imported
438
+ * from the artifact url. Exact by construction - it is the same list the browser will ask for.
439
+ */
440
+ export function demandedFromOutputs(runtime: PrebuiltRuntime, outDir: string) {
441
+ const moduleUrl = prebuiltModuleUrl(runtime);
442
+ const byMangled = new Map<string, { specifier: string; name: string }>();
443
+ for (const [specifier, names] of runtime.exports) {
444
+ for (const name of names) byMangled.set(mangledName(specifier, name), { specifier, name });
445
+ }
446
+ const demanded = new Map<string, Set<string>>();
447
+ const walk = (dir: string) => {
448
+ let entries: Dirent[];
449
+ try {
450
+ entries = readdirSync(dir, { withFileTypes: true });
451
+ } catch {
452
+ return;
453
+ }
454
+ for (const entry of entries) {
455
+ const file = path.join(dir, entry.name);
456
+ // The artifact itself lives under the out dir in prod; it is where these
457
+ // names come from, not a consumer of them.
458
+ if (entry.isDirectory()) {
459
+ if (file !== runtime.dir) walk(file);
460
+ continue;
461
+ }
462
+ if (!entry.name.endsWith('.js')) continue;
463
+ let source: string;
464
+ try {
465
+ source = readFileSync(file, 'utf8');
466
+ } catch {
467
+ continue;
468
+ }
469
+ if (!source.includes(moduleUrl)) continue;
470
+ for (const edge of scanFacts(file, source).imports) {
471
+ if (edge.specifier !== moduleUrl) continue;
472
+ for (const bound of edge.exports) {
473
+ const origin = byMangled.get(bound);
474
+ if (!origin) continue;
475
+ const names = demanded.get(origin.specifier) ?? new Set<string>();
476
+ demanded.set(origin.specifier, names);
477
+ names.add(origin.name);
478
+ }
479
+ }
480
+ }
481
+ };
482
+ walk(outDir);
483
+ return demanded;
484
+ }
485
+
486
+ /** Is every demanded name already in the artifact on disk? */
487
+ export function surfaceCovered(runtime: PrebuiltRuntime, demanded: Map<string, Set<string>>) {
488
+ for (const [specifier, names] of demanded) {
489
+ const have = runtime.surface.get(specifier);
490
+ if (!have) return false;
491
+ for (const name of names) if (!have.has(name)) return false;
492
+ }
493
+ return true;
494
+ }
495
+
496
+ const manifestName = 'pnext-runtime.json';
497
+
498
+ /**
499
+ * One artifact address is shared by every route build in the process, and in
500
+ * dev several routes compile at once. Serialize per address so two esbuild runs
501
+ * never write the same directory concurrently.
502
+ */
503
+ const artifactLocks = new Map<string, Promise<unknown>>();
504
+
505
+ function underLock<T>(dir: string, run: () => Promise<T>): Promise<T> {
506
+ const previous = artifactLocks.get(dir) ?? Promise.resolve();
507
+ const next = previous.then(run, run);
508
+ artifactLocks.set(dir, next.catch(() => undefined));
509
+ return next;
510
+ }
511
+
512
+ /**
513
+ * Open the artifact at this address, building a seed one if there is none. The
514
+ * seed probes the vendor specifiers: those are the single-instance modules, so
515
+ * they must be prebuilt from the very first route build. Everything else is
516
+ * probed and folded in by the build that first demands it.
517
+ */
518
+ export function ensurePrebuiltRuntime(options: ArtifactOptions) {
519
+ return underLock(options.dir, () => openPrebuiltRuntime(options));
520
+ }
521
+
522
+ async function openPrebuiltRuntime(options: ArtifactOptions) {
523
+ const manifest = readManifest(options.dir);
524
+ if (manifest) {
525
+ return {
526
+ key: options.key,
527
+ dir: options.dir,
528
+ publicPath: options.publicPath,
529
+ assetPath: options.assetPath,
530
+ ...manifest,
531
+ } satisfies PrebuiltRuntime;
532
+ }
533
+ const exportsBySpecifier = await probeSurface(seedSpecifiers(options.aliases), options);
534
+ // Seed surface: nothing is demanded yet, so the aggregate starts empty and
535
+ // the first route build's own output says what belongs in it.
536
+ const runtime = await buildPrebuiltRuntime(options, exportsBySpecifier, new Map());
537
+ await writeManifest(runtime);
538
+ return runtime;
539
+ }
540
+ /**
541
+ * Fold what the build demanded - and any specifier it met for the first time - into the artifact. A
542
+ * no-op in the steady state, which is every build after an app's client imports stop changing.
543
+ */
544
+ export function settlePrebuiltRuntime(
545
+ runtime: PrebuiltRuntime,
546
+ options: ArtifactOptions,
547
+ outDir: string,
548
+ unprobed: Set<string>,
549
+ ) {
550
+ return underLock(options.dir, () => growPrebuiltRuntime(runtime, options, outDir, unprobed));
551
+ }
552
+
553
+ async function growPrebuiltRuntime(
554
+ runtime: PrebuiltRuntime,
555
+ options: ArtifactOptions,
556
+ outDir: string,
557
+ unprobed: Set<string>,
558
+ ) {
559
+ // Another build may have grown the artifact while this one ran; merge onto
560
+ // what is on disk now, not onto the snapshot this build opened.
561
+ const current = readManifest(options.dir);
562
+ if (current) runtime = { ...runtime, ...current };
563
+ const demanded = demandedFromOutputs(runtime, outDir);
564
+ if (unprobed.size === 0 && surfaceCovered(runtime, demanded)) return runtime;
565
+ const exportsBySpecifier = new Map(runtime.exports);
566
+ for (const [specifier, names] of await probeSurface([...unprobed], options)) {
567
+ exportsBySpecifier.set(specifier, names);
568
+ }
569
+ const surface = new Map(runtime.surface);
570
+ for (const [specifier, names] of demanded) {
571
+ const target = surface.get(specifier) ?? new Set<string>();
572
+ surface.set(specifier, target);
573
+ for (const name of names) target.add(name);
574
+ }
575
+ const grown = await buildPrebuiltRuntime(options, exportsBySpecifier, surface);
576
+ await writeManifest(grown);
577
+ return grown;
578
+ }
579
+
580
+ interface Manifest {
581
+ key: string;
582
+ exports: Record<string, string[]>;
583
+ surface: Record<string, string[]>;
584
+ closure: { static: string[]; dynamic: string[] };
585
+ }
586
+
587
+ function readManifest(dir: string) {
588
+ try {
589
+ const parsed = JSON.parse(readFileSync(path.join(dir, manifestName), 'utf8')) as Manifest;
590
+ if (!parsed.exports) return undefined;
591
+ return {
592
+ exports: new Map(Object.entries(parsed.exports)),
593
+ surface: new Map(Object.entries(parsed.surface).map(([spec, n]) => [spec, new Set(n)])),
594
+ closure: parsed.closure,
595
+ };
596
+ } catch {
597
+ return undefined;
598
+ }
599
+ }
600
+
601
+ async function writeManifest(runtime: PrebuiltRuntime) {
602
+ const manifest: Manifest = {
603
+ key: runtime.key,
604
+ exports: Object.fromEntries(runtime.exports),
605
+ surface: Object.fromEntries([...runtime.surface].map(([s, n]) => [s, [...n].sort()])),
606
+ closure: runtime.closure,
607
+ };
608
+ await writeText(path.join(runtime.dir, manifestName), JSON.stringify(manifest));
609
+ }
610
+
611
+ /**
612
+ * The artifact files a route entry importing the aggregate must preload, as
613
+ * `assets/…` names — what the rendered page's modulepreload list is written in.
614
+ */
615
+ export function prebuiltAssets(runtime: PrebuiltRuntime) {
616
+ const join = (name: string) => `${runtime.assetPath}/${name}`;
617
+ return {
618
+ static: [join(PREBUILT_MODULE), ...runtime.closure.static.map(join)],
619
+ dynamic: runtime.closure.dynamic.map(join),
620
+ };
621
+ }