@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,721 @@
1
+ // Content-addressed naming and O(1) validity for dev's compiled server modules.
2
+ //
3
+ // An artifact's name carries a hash of its own source AND of every source reachable from it, so an edit
4
+ // renames exactly the edited file and the modules that import it transitively - everything else keeps its
5
+ // name, stays on disk, and is neither recompiled nor re-imported. Staleness is therefore a cache miss,
6
+ // never a stale hit, which is what lets the cache survive both restarts and saves.
7
+ //
8
+ // The graph the hash walks is read from a persisted index keyed by (mtime, size, content hash), so a
9
+ // restart re-derives it with one stat per source instead of re-scanning and re-resolving every import.
10
+ import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
11
+ import { cachedExistsSync } from '../utils/fs-cache';
12
+ import { mkdir, readFile, stat } from 'node:fs/promises';
13
+ import { createHash, randomUUID } from 'node:crypto';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import type { ResolvedConfig } from '../config';
17
+ import { writeFileAtomic } from '../utils/fs';
18
+
19
+ /**
20
+ * Kill switch for the request-head trims: the naming walk's synchronous source
21
+ * reads and the layer recorded alongside a source's edges.
22
+ */
23
+ export function devHeadTrimEnabled(): boolean {
24
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
25
+ return process.env.PNEXT_HEAD_TRIM !== '0';
26
+ }
27
+
28
+ /** Files whose bytes are not source: hashed from stat, never read or scanned. */
29
+ const BINARY_SOURCE = /\.(?:png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|otf|eot|mp[34]|webm|pdf)$/i;
30
+
31
+ const MISSING = 'missing';
32
+
33
+ // The framework's own package root. Compat runtime modules (next/form and
34
+ // friends) are compiled like app sources but live outside the workspace, so
35
+ // they need their own relocation anchor.
36
+ const frameworkRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
37
+
38
+ /**
39
+ * A source's identity for hashing and for the persisted index - relative to the workspace (or the
40
+ * framework) rather than absolute. Absolute paths made every artifact name a function of *where the
41
+ * project sat* on disk: invisible in dev, and fatal for a deployed build, where the vercel adapter
42
+ * ships the compiled cache and the function replays it from the serverless runtime root, so every
43
+ * recomputed name differed, the whole cache missed, and the runtime recompiled the app on the first
44
+ * request against a read-only filesystem.
45
+ */
46
+ // Two `path.relative` calls per source per lookup, and every graph hash asks for its whole closure.
47
+ // The answer is a pure function of the pair and the set of sources is bounded by the app, so it is
48
+ // simply remembered.
49
+ const portablePaths = new Map<string, string>();
50
+ // Same bisect seam as the naming walk it serves (PNEXT_DEV_GRAPH_FAST=0).
51
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
52
+ const memoPortablePaths = process.env.PNEXT_DEV_GRAPH_FAST !== '0';
53
+
54
+ function portableSourcePath(file: string, workspaceRoot: string) {
55
+ if (!memoPortablePaths) return computePortableSourcePath(file, workspaceRoot);
56
+ const memoKey = `${workspaceRoot}\0${file}`;
57
+ const memoized = portablePaths.get(memoKey);
58
+ if (memoized !== undefined) return memoized;
59
+ const computed = computePortableSourcePath(file, workspaceRoot);
60
+ portablePaths.set(memoKey, computed);
61
+ return computed;
62
+ }
63
+
64
+ function computePortableSourcePath(file: string, workspaceRoot: string) {
65
+ const inWorkspace = path.relative(workspaceRoot, file);
66
+ if (inWorkspace && !inWorkspace.startsWith('..') && !path.isAbsolute(inWorkspace)) {
67
+ return `w:${toPosix(inWorkspace)}`;
68
+ }
69
+ const inFramework = path.relative(frameworkRoot, file);
70
+ if (inFramework && !inFramework.startsWith('..') && !path.isAbsolute(inFramework)) {
71
+ return `f:${toPosix(inFramework)}`;
72
+ }
73
+ return `a:${toPosix(file)}`;
74
+ }
75
+
76
+ /** Inverse of `portableSourcePath` for the roots this process is running under. */
77
+ function absoluteSourcePath(key: string, workspaceRoot: string) {
78
+ const value = key.slice(2).split('/').join(path.sep);
79
+ if (key.startsWith('w:')) return path.resolve(workspaceRoot, value);
80
+ if (key.startsWith('f:')) return path.resolve(frameworkRoot, value);
81
+ return value;
82
+ }
83
+
84
+ const toPosix = (file: string) => (path.sep === '/' ? file : file.split(path.sep).join('/'));
85
+
86
+ /** Stable identity for a source outside the dev cache — see `portableSourcePath`. */
87
+ export function devSourceIdentity(file: string, workspaceRoot: string) {
88
+ return portableSourcePath(file, workspaceRoot);
89
+ }
90
+
91
+ interface SourceRecord {
92
+ mtimeMs: number;
93
+ size: number;
94
+ /** Content hash (stat hash for binaries, `missing` when the file is gone). */
95
+ srcHash: string;
96
+ /** Resolved local import targets — the same edges the compile walk follows. */
97
+ imports: string[];
98
+ /** Bare package specifiers, so a route's vendor demand is known before it compiles. */
99
+ packages: string[];
100
+ /** Whether the source opens with `'use client'` — the layer boundary, recorded by the scan. */
101
+ client: boolean;
102
+ }
103
+
104
+ export interface DevModuleCacheOptions {
105
+ /** Local import targets, package specifiers and layer of `source`, as the compile walk sees them. */
106
+ edges: (file: string, source: string) => { imports: string[]; packages: string[]; client: boolean };
107
+ /** Everything outside the sources that changes output: aliases, defines, compat. */
108
+ compileKey: string;
109
+ }
110
+
111
+ /** `[file, mtimeMs, size, srcHash]` — one indexed source, as callers revalidate it. */
112
+ export type DevGraphSource = [string, number, number, string];
113
+
114
+ /** `srcHash` for a source outside the index — `missing` when it cannot be read. */
115
+ export const DEV_SOURCE_MISSING = MISSING;
116
+
117
+ /** Files whose `srcHash` comes from stat rather than content (see BINARY_SOURCE). */
118
+ export function isBinaryDevSource(file: string): boolean {
119
+ return BINARY_SOURCE.test(file);
120
+ }
121
+
122
+ /** The `srcHash` rule for a text source, for caches revalidating indexed records. */
123
+ export function devTextSourceHash(source: string): string {
124
+ return Bun.hash(source).toString(36);
125
+ }
126
+
127
+ export interface DevModuleCache {
128
+ /** Hash naming `file`'s artifact: its own source plus its whole source graph. */
129
+ graphHash(file: string): Promise<string>;
130
+ /** Everything the last `graphHash` of these files walked, as indexed records. */
131
+ graphSources(files: string[]): Promise<DevGraphSource[]>;
132
+ /** Everything outside the sources that names an artifact — compat, aliases, pnext itself. */
133
+ readonly graphKey: string;
134
+ /** Forget the given sources and every module that reaches them (its dependents). */
135
+ invalidate(files: Iterable<string>): void;
136
+ /** Whether this source is already part of the compiled graph. */
137
+ knows(file: string): boolean;
138
+ /**
139
+ * Whether this source opens with `'use client'`, from the record the walk already scanned - the
140
+ * layer-aware seed used to re-read and re-scan every source of the route to learn the same thing, on the
141
+ * request the response is blocked on.
142
+ */
143
+ isClientSource(file: string): Promise<boolean>;
144
+ /**
145
+ * Every package specifier reachable from `file` - the route's vendor demand. With `prune`, a SEPARATE
146
+ * non-memoized walk that skips pruned files and their subtrees (layer-aware seeding); `closure()` keeps
147
+ * its superset for invalidation.
148
+ */
149
+ packageDemand(file: string, prune?: (file: string) => boolean | Promise<boolean>): Promise<string[]>;
150
+ /**
151
+ * Freeze names for the duration of one compile pass: each source is re-checked once when the pass first
152
+ * touches it, never again while it runs. A hash that moved mid-pass would let a module bake an import
153
+ * href its target is then never written to - a dangling import Bun caches for the life of the process.
154
+ */
155
+ hold(): () => void;
156
+ /** Directory the compiled artifacts live in. */
157
+ readonly root: string;
158
+ }
159
+
160
+ const caches = new Map<string, DevModuleCache>();
161
+
162
+ /** The cache for `config.outPath`, created once per process. */
163
+ export function devModuleCache(
164
+ config: ResolvedConfig,
165
+ options: DevModuleCacheOptions,
166
+ ): DevModuleCache {
167
+ const root = cacheRoot(config.outPath);
168
+ const existing = caches.get(root);
169
+ if (existing) return existing;
170
+ const created = createDevModuleCache(root, config.workspaceRoot, options);
171
+ caches.set(root, created);
172
+ return created;
173
+ }
174
+
175
+ export function cacheRoot(outPath: string) {
176
+ return path.join(outPath, 'cache', 'server');
177
+ }
178
+
179
+ function createDevModuleCache(
180
+ root: string,
181
+ workspaceRoot: string,
182
+ options: DevModuleCacheOptions,
183
+ ): DevModuleCache {
184
+ const indexFile = path.join(root, 'graph.json');
185
+ // Compiler-generated sources — pnext's own output tree and the materialized
186
+ // pages-compat app — are rewritten from scratch on every boot and can land at
187
+ // a fresh path each time; they are hashed by content alone (below).
188
+ const outRoot = `${path.dirname(path.dirname(root))}${path.sep}`;
189
+ const generated = (file: string) =>
190
+ file.startsWith(outRoot) || file.includes(`${path.sep}pnext-pages-compat${path.sep}`);
191
+ const key = createHash('sha256')
192
+ .update(`${options.compileKey}\0${frameworkFingerprint()}`)
193
+ .digest('hex')
194
+ .slice(0, 16);
195
+ establishCacheTrust(root);
196
+ const records = indexRecords(readIndexFile(indexFile), key, workspaceRoot);
197
+ // PNEXT_DEV_CACHE_DEBUG=1 names every source whose hash differs from the one
198
+ // the last boot persisted — i.e. exactly why an artifact was renamed and
199
+ // recompiled instead of reused.
200
+ const booted =
201
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
202
+ process.env.PNEXT_DEV_CACHE_DEBUG && records.size > 0
203
+ ? new Map([...records].map(([file, record]) => [file, record.srcHash] as const))
204
+ : undefined;
205
+ const pending = new Map<string, Promise<SourceRecord>>();
206
+ // Records already re-checked in the current pass - what `hold()` promises ("re-checked once when
207
+ // the pass first touches it, never again while it runs") and what `pending` alone does not
208
+ // deliver: it drops each entry the moment the stat settles, so naming a route re-stat'ed its whole
209
+ // closure once for the reachable-set walk and again for the hash.
210
+ // Bisect seam: `PNEXT_DEV_GRAPH_FAST=0` restores the previous cost model - re-stat every touch,
211
+ // one module per turn, no path memo.
212
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
213
+ const graphFast = process.env.PNEXT_DEV_GRAPH_FAST !== '0';
214
+ const settledThisPass = new Map<string, Promise<SourceRecord>>();
215
+ const refreshedThisPass = new Set<string>();
216
+ let passes = 0;
217
+ const closures = new Map<string, Set<string>>();
218
+ const hashes = new Map<string, Promise<string>>();
219
+ let dirty = false;
220
+ let flush: Timer | undefined;
221
+ let retries = 0;
222
+
223
+ function persist() {
224
+ if (flush) return;
225
+ flush = setTimeout(async () => {
226
+ flush = undefined;
227
+ if (!dirty) return;
228
+ const contents = serializeIndex(key, records, workspaceRoot);
229
+ try {
230
+ await mkdir(root, { recursive: true });
231
+ await writeFileAtomic(indexFile, contents);
232
+ dirty = false;
233
+ } catch {
234
+ // A write that lost a race with a cache wipe must not look like a
235
+ // persisted index: keep `dirty` and try again a couple of times.
236
+ if (++retries < 3) persist();
237
+ }
238
+ }, 50);
239
+ // The index is a pure optimization; never hold the process open for it.
240
+ flush.unref?.();
241
+ }
242
+
243
+ // A short-lived process (a one-off compile, a build) can exit before the
244
+ // debounce fires; without this its work would not survive to the next boot.
245
+ process.on('exit', () => {
246
+ if (!dirty) return;
247
+ dirty = false;
248
+ try {
249
+ writeFileSync(indexFile, serializeIndex(key, records, workspaceRoot));
250
+ } catch {
251
+ // Best effort: a missing index only costs the next boot a rescan.
252
+ }
253
+ });
254
+
255
+ async function sourceRecord(file: string): Promise<SourceRecord> {
256
+ const inflight = pending.get(file) ?? (graphFast && passes > 0 ? settledThisPass.get(file) : undefined);
257
+ if (inflight) return inflight;
258
+ const previous = records.get(file);
259
+ const next = readSourceRecord(file, previous, options.edges).then(record => {
260
+ pending.delete(file);
261
+ if (record === previous) return record;
262
+ records.set(file, record);
263
+ dirty = true;
264
+ persist();
265
+ return record;
266
+ });
267
+ pending.set(file, next);
268
+ if (graphFast && passes > 0) settledThisPass.set(file, next);
269
+ return next;
270
+ }
271
+
272
+ /**
273
+ * Re-stat a memoized closure before trusting it. The watcher normally reports
274
+ * a save first, but it is best-effort (recursive watch is not available
275
+ * everywhere), and the disk is the only authority on what a module contains.
276
+ */
277
+ async function refresh(file: string) {
278
+ const reachable = closures.get(file);
279
+ if (!reachable) return;
280
+ if (passes > 0 && refreshedThisPass.has(file)) return;
281
+ refreshedThisPass.add(file);
282
+ const changed: string[] = [];
283
+ await Promise.all(
284
+ [...reachable].map(async source => {
285
+ const previous = records.get(source);
286
+ const record = await sourceRecord(source);
287
+ if (previous?.srcHash !== record.srcHash) changed.push(source);
288
+ }),
289
+ );
290
+ if (changed.length > 0) forget(changed);
291
+ }
292
+
293
+ /** Drop the memoized graph of every module that reaches one of `changed`. */
294
+ function forget(changed: string[]) {
295
+ for (const [source, reachable] of closures) {
296
+ if (!changed.some(target => reachable.has(target))) continue;
297
+ closures.delete(source);
298
+ hashes.delete(source);
299
+ }
300
+ }
301
+
302
+ /** The pre-batching walk and hash gather, kept behind PNEXT_DEV_GRAPH_FAST=0. */
303
+ async function serialFrontier(frontier: string[], file: string) {
304
+ const reached: { expand: boolean; targets: Iterable<string> }[] = [];
305
+ for (const current of frontier) {
306
+ const known = current === file ? undefined : closures.get(current);
307
+ reached.push(
308
+ known
309
+ ? { expand: false, targets: known as Iterable<string> }
310
+ : { expand: true, targets: (await sourceRecord(current)).imports },
311
+ );
312
+ }
313
+ return reached;
314
+ }
315
+
316
+ async function serialRecords(reachable: string[]) {
317
+ const records: SourceRecord[] = [];
318
+ for (const source of reachable) records.push(await sourceRecord(source));
319
+ return records;
320
+ }
321
+
322
+ /** Every source reachable from `file`, itself included. Cycle-safe by construction. */
323
+ async function closure(file: string): Promise<Set<string>> {
324
+ const cached = closures.get(file);
325
+ if (cached) return cached;
326
+ const seen = new Set<string>([file]);
327
+ // A whole frontier per turn, not one module per turn: the walk is pure I/O latency (one stat per
328
+ // module), so taking it in series turns a route graph into that many sequential round trips, on
329
+ // the request the response is blocked on.
330
+ let frontier = [file];
331
+ while (frontier.length > 0) {
332
+ const reached = graphFast
333
+ ? await Promise.all(
334
+ frontier.map(async current => {
335
+ // A complete reachable set stays complete however it was reached, so a
336
+ // memoized child is merged instead of re-expanded.
337
+ const known = current === file ? undefined : closures.get(current);
338
+ return known
339
+ ? { expand: false, targets: known as Iterable<string> }
340
+ : { expand: true, targets: (await sourceRecord(current)).imports };
341
+ }),
342
+ )
343
+ : await serialFrontier(frontier, file);
344
+ const next: string[] = [];
345
+ for (const { expand, targets } of reached) {
346
+ for (const target of targets) {
347
+ if (seen.has(target)) continue;
348
+ seen.add(target);
349
+ if (expand) next.push(target);
350
+ }
351
+ }
352
+ frontier = next;
353
+ }
354
+ closures.set(file, seen);
355
+ return seen;
356
+ }
357
+
358
+ async function computeGraphHash(file: string) {
359
+ const reachable = [...(await closure(file))].sort();
360
+ // Same reason as the walk above: gather every record first, then fold them
361
+ // in sorted order, instead of one awaited stat between each hash update.
362
+ const records: SourceRecord[] = graphFast
363
+ ? await Promise.all(reachable.map(source => sourceRecord(source)))
364
+ : await serialRecords(reachable);
365
+ const hash = createHash('sha256').update(key).update('\0');
366
+ const moved: string[] = [];
367
+ for (const [index, source] of reachable.entries()) {
368
+ const record = records[index]!;
369
+ // A generated module (materialized pages wrapper, bundled config) can
370
+ // live at a path that changes between boots while its content does not;
371
+ // naming it by content alone keeps its dependents' names stable.
372
+ hash.update(generated(source) ? '' : portableSourcePath(source, workspaceRoot));
373
+ hash.update('\0');
374
+ hash.update(record.srcHash);
375
+ hash.update('\0');
376
+ if (booted && booted.get(source) !== record.srcHash) moved.push(source);
377
+ }
378
+ if (booted && moved.length > 0) {
379
+ console.log(
380
+ `dev-cache ${path.basename(file)} renamed: ${moved.length} source(s) differ from the persisted graph\n ${moved
381
+ .slice(0, 10)
382
+ .join('\n ')}`,
383
+ );
384
+ }
385
+ return hash.digest('hex').slice(0, 16);
386
+ }
387
+
388
+ return {
389
+ root,
390
+ graphKey: key,
391
+ async graphSources(files) {
392
+ const reachable = new Set<string>();
393
+ for (const file of files) for (const source of await closure(path.resolve(file))) reachable.add(source);
394
+ return Promise.all(
395
+ [...reachable].sort().map(async (source): Promise<DevGraphSource> => {
396
+ const record = await sourceRecord(source);
397
+ return [source, record.mtimeMs, record.size, record.srcHash];
398
+ }),
399
+ );
400
+ },
401
+ async graphHash(file) {
402
+ const resolved = path.resolve(file);
403
+ await refresh(resolved);
404
+ const existing = hashes.get(resolved);
405
+ if (existing) return existing;
406
+ const next = computeGraphHash(resolved).catch(error => {
407
+ hashes.delete(resolved);
408
+ throw error;
409
+ });
410
+ hashes.set(resolved, next);
411
+ return next;
412
+ },
413
+ knows(file) {
414
+ return records.has(path.resolve(file));
415
+ },
416
+ async isClientSource(file) {
417
+ return (await sourceRecord(path.resolve(file))).client;
418
+ },
419
+ async packageDemand(file, prune) {
420
+ const resolved = path.resolve(file);
421
+ const demand = new Set<string>();
422
+ if (!prune) {
423
+ for (const source of await closure(resolved)) {
424
+ for (const name of (await sourceRecord(source)).packages) demand.add(name);
425
+ }
426
+ return [...demand];
427
+ }
428
+ const seen = new Set<string>([resolved]);
429
+ let frontier = [resolved];
430
+ while (frontier.length > 0) {
431
+ const kept = (
432
+ await Promise.all(frontier.map(async source => ((await prune(source)) ? undefined : source)))
433
+ ).filter((source): source is string => source !== undefined);
434
+ const reached = await Promise.all(kept.map(source => sourceRecord(source)));
435
+ const next: string[] = [];
436
+ for (const record of reached) {
437
+ for (const name of record.packages) demand.add(name);
438
+ for (const target of record.imports) {
439
+ if (seen.has(target)) continue;
440
+ seen.add(target);
441
+ next.push(target);
442
+ }
443
+ }
444
+ frontier = next;
445
+ }
446
+ return [...demand];
447
+ },
448
+ hold() {
449
+ if (passes++ === 0) {
450
+ refreshedThisPass.clear();
451
+ settledThisPass.clear();
452
+ }
453
+ let released = false;
454
+ return () => {
455
+ if (released) return;
456
+ released = true;
457
+ // Outside a pass the disk is the only authority again, so nothing may
458
+ // survive the last release.
459
+ if (--passes === 0) settledThisPass.clear();
460
+ };
461
+ },
462
+ invalidate(files) {
463
+ const changed = new Set([...files].map(file => path.resolve(file)));
464
+ if (changed.size === 0) return;
465
+ for (const file of changed) {
466
+ records.delete(file);
467
+ pending.delete(file);
468
+ // A save lands mid-pass often enough to matter: the pass must not go on
469
+ // reusing the record the watcher just invalidated.
470
+ settledThisPass.delete(file);
471
+ dirty = true;
472
+ }
473
+ // Dependents are exactly the modules whose reachable set holds a changed
474
+ // file: drop their memoized graph, keep every unrelated module's.
475
+ forget([...changed]);
476
+ persist();
477
+ },
478
+ };
479
+ }
480
+
481
+ const missingRecord = (): SourceRecord => ({
482
+ mtimeMs: 0,
483
+ size: 0,
484
+ srcHash: MISSING,
485
+ imports: [],
486
+ packages: [],
487
+ client: false,
488
+ });
489
+
490
+ // Sync reads skip the fs thread-pool queue the vendor preloads own - the walk is cheap standing
491
+ // alone but slow once queued behind them.
492
+ async function readSourceRecord(
493
+ file: string,
494
+ previous: SourceRecord | undefined,
495
+ edges: DevModuleCacheOptions['edges'],
496
+ ): Promise<SourceRecord> {
497
+ const sync = devHeadTrimEnabled();
498
+ const stats = sync
499
+ ? (() => { try { return statSync(file); } catch { return undefined; } })()
500
+ : await stat(file).catch(() => undefined);
501
+ if (!stats) return missingRecord();
502
+ // Fast path: an untouched file keeps the indexed hash and edge list, so a
503
+ // restart pays one stat per source instead of a read + scan + resolve.
504
+ if (previous?.mtimeMs === stats.mtimeMs && previous.size === stats.size) {
505
+ return previous;
506
+ }
507
+ if (isBinaryDevSource(file)) {
508
+ return {
509
+ mtimeMs: stats.mtimeMs,
510
+ size: stats.size,
511
+ srcHash: `b${stats.size.toString(36)}.${Math.round(stats.mtimeMs).toString(36)}`,
512
+ imports: [],
513
+ packages: [],
514
+ client: false,
515
+ };
516
+ }
517
+ const source = sync
518
+ ? (() => { try { return readFileSync(file, 'utf8'); } catch { return undefined; } })()
519
+ : await readFile(file, 'utf8').catch(() => undefined);
520
+ if (source === undefined) return missingRecord();
521
+ const srcHash = devTextSourceHash(source);
522
+ // Content decides: a touched-but-unchanged file keeps its edges (and so its
523
+ // dependents keep their names) even though its mtime moved.
524
+ const scanned = previous?.srcHash === srcHash ? previous : edges(file, source);
525
+ return {
526
+ mtimeMs: stats.mtimeMs,
527
+ size: stats.size,
528
+ srcHash,
529
+ imports: scanned.imports,
530
+ packages: scanned.packages,
531
+ client: scanned.client,
532
+ };
533
+ }
534
+
535
+ // --------------------------------------------------------------------------
536
+ // cache marker — the O(1) "was this folder wiped?" check
537
+ // --------------------------------------------------------------------------
538
+
539
+ // A build empties `.pnext` under running dev servers, which can leave an artifact on disk whose
540
+ // `cache/server/` imports were deleted; Bun caches a failed import for the life of the process, so a
541
+ // dangling edge must never reach the runtime. Instead of reading back every compiled module and walking its
542
+ // edges, the folder carries a uuid marker: while it still reads back as the value this process adopted at
543
+ // boot, the folder was never wiped and every artifact still in it still has its edges. Once it does not -
544
+ // or once there is no marker at all - nothing on disk is trusted until this process rewrites it.
545
+ const markers = new Map<string, string>();
546
+ const wiped = new Set<string>();
547
+ const written = new Set<string>();
548
+ const markerChecks = new Map<string, number>();
549
+
550
+ function markerFile(root: string) {
551
+ return path.join(root, '.cache-id');
552
+ }
553
+
554
+ /**
555
+ * Adopt the folder's marker. Its presence is the claim that no `pnext build`
556
+ * has emptied the folder since a server last compiled into it, so what is in
557
+ * there still has its edges; a folder without one starts untrusted.
558
+ */
559
+ function establishCacheTrust(root: string) {
560
+ const current = readMarker(root);
561
+ const value = current ?? randomUUID();
562
+ if (!current) {
563
+ writeMarker(root, value);
564
+ wiped.add(root);
565
+ }
566
+ markers.set(root, value);
567
+ return value;
568
+ }
569
+
570
+ function readMarker(root: string) {
571
+ try {
572
+ return readFileSync(markerFile(root), 'utf8').trim() || undefined;
573
+ } catch {
574
+ return undefined;
575
+ }
576
+ }
577
+
578
+ function writeMarker(root: string, value: string) {
579
+ try {
580
+ mkdirSync(root, { recursive: true });
581
+ writeFileSync(markerFile(root), value);
582
+ } catch {
583
+ // Non-fatal: an unwritable marker only costs a recompile.
584
+ }
585
+ }
586
+
587
+ /**
588
+ * True while the folder still carries this process's marker. Re-reads at most every 100 ms - the
589
+ * window it leaves is a wipe racing an in-flight compile, which recompiles into place anyway.
590
+ */
591
+ function cacheMarkerIntact(root: string) {
592
+ const recorded = markers.get(root);
593
+ if (recorded === undefined || wiped.has(root)) return false;
594
+ const now = performance.now();
595
+ if (now - (markerChecks.get(root) ?? -Infinity) < 100) return true;
596
+ markerChecks.set(root, now);
597
+ const current = readMarker(root);
598
+ if (current === recorded) return true;
599
+ // Wiped under us: re-establish the marker so the rebuilt cache is reusable
600
+ // next boot, but distrust everything still on disk for the rest of this run.
601
+ if (!current) writeMarker(root, recorded);
602
+ else markers.set(root, current);
603
+ wiped.add(root);
604
+ return false;
605
+ }
606
+
607
+ /** Record an artifact this process just wrote — trusted even after a wipe. */
608
+ export function noteDevArtifactWritten(file: string) {
609
+ const root = artifactCacheRoot(file);
610
+ if (root && wiped.has(root)) written.add(file);
611
+ }
612
+
613
+ /**
614
+ * Can this compiled artifact be imported as-is? One `existsSync` (a hand-deleted file) plus the folder
615
+ * marker (a build wipe) - the whole-graph read walk this replaces cost one read and one stat per edge, per
616
+ * module, per boot.
617
+ */
618
+ export function devArtifactUsable(file: string) {
619
+ if (!cachedExistsSync(file)) return false;
620
+ const root = artifactCacheRoot(file);
621
+ // No dev module cache for this folder — a prod build writes compiled output
622
+ // here too, and it owns the directory it just emptied.
623
+ if (!root || !markers.has(root)) return true;
624
+ return cacheMarkerIntact(root) || written.has(file);
625
+ }
626
+
627
+ function artifactCacheRoot(file: string) {
628
+ const marker = `${path.sep}cache${path.sep}server${path.sep}`;
629
+ const index = file.lastIndexOf(marker);
630
+ return index === -1 ? undefined : file.slice(0, index + marker.length - 1);
631
+ }
632
+
633
+ // --------------------------------------------------------------------------
634
+ // persisted graph index
635
+ // --------------------------------------------------------------------------
636
+
637
+ interface IndexFile {
638
+ version: number;
639
+ key: string;
640
+ files: Record<string, [number, number, string, string[], string[], 0 | 1]>;
641
+ }
642
+
643
+ /** Bumped whenever a record's shape changes; an older index is simply ignored. */
644
+ const INDEX_VERSION = 4;
645
+
646
+ function readIndexFile(file: string): IndexFile | undefined {
647
+ try {
648
+ return JSON.parse(readFileSync(file, 'utf8')) as IndexFile;
649
+ } catch {
650
+ return undefined;
651
+ }
652
+ }
653
+
654
+ // Source hashes and edges stay usable even when the artifacts they named are
655
+ // gone — only a different compile key makes them describe another cache.
656
+ // Paths are stored portably (see `portableSourcePath`) so an index written on
657
+ // the build machine still describes the same sources under the deployment root.
658
+ function indexRecords(parsed: IndexFile | undefined, key: string, workspaceRoot: string) {
659
+ const records = new Map<string, SourceRecord>();
660
+ if (parsed?.key !== key || parsed.version !== INDEX_VERSION) return records;
661
+ for (const [source, entry] of Object.entries(parsed.files ?? {})) {
662
+ records.set(absoluteSourcePath(source, workspaceRoot), {
663
+ mtimeMs: entry[0],
664
+ size: entry[1],
665
+ srcHash: entry[2],
666
+ imports: entry[3].map(file => absoluteSourcePath(file, workspaceRoot)),
667
+ packages: entry[4] ?? [],
668
+ client: entry[5] === 1,
669
+ });
670
+ }
671
+ return records;
672
+ }
673
+
674
+ function serializeIndex(key: string, records: Map<string, SourceRecord>, workspaceRoot: string) {
675
+ const files: IndexFile['files'] = {};
676
+ for (const [source, record] of records) {
677
+ files[portableSourcePath(source, workspaceRoot)] = [
678
+ record.mtimeMs,
679
+ record.size,
680
+ record.srcHash,
681
+ record.imports.map(file => portableSourcePath(file, workspaceRoot)),
682
+ record.packages,
683
+ record.client ? 1 : 0,
684
+ ];
685
+ }
686
+ return JSON.stringify({ version: INDEX_VERSION, key, files } satisfies IndexFile);
687
+ }
688
+
689
+ // pnext's own compiler sources shape every artifact, so editing the framework must invalidate the cache the
690
+ // same way editing app code does - but ONLY an edit. Fingerprinting mtimes threw the whole cache away
691
+ // whenever a file was merely touched (a reinstall, a checkout, a staged copy, a no-op save), so this reads
692
+ // the files that actually emit compiled output. A few dozen small files, once per process, off the boot
693
+ // path - the first compile pays it.
694
+ let fingerprint: string | undefined;
695
+
696
+ function frameworkFingerprint() {
697
+ if (fingerprint !== undefined) return fingerprint;
698
+ const hash = createHash('sha256');
699
+ for (const dir of ['.', '..', '../runtime', '../compat/bundler', '../resolve']) {
700
+ const root = new URL(`${dir}/`, import.meta.url).pathname;
701
+ let entries: string[];
702
+ try {
703
+ entries = readdirSync(root).sort();
704
+ } catch {
705
+ continue;
706
+ }
707
+ for (const entry of entries) {
708
+ // Declarations emit nothing, and a deployed build ships without them
709
+ // (the vercel adapter drops `.d.ts`) — hashing one would key the cache to
710
+ // a file that is provably absent where the artifacts are replayed.
711
+ if (!entry.endsWith('.ts') || /\.d\.[cm]?ts$/.test(entry)) continue;
712
+ try {
713
+ hash.update(`${entry}:${Bun.hash(readFileSync(path.join(root, entry))).toString(36)}\0`);
714
+ } catch {
715
+ // ignore unreadable entries
716
+ }
717
+ }
718
+ }
719
+ fingerprint = hash.digest('hex').slice(0, 16);
720
+ return fingerprint;
721
+ }