@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,375 @@
1
+ import {
2
+ decodeSegmentPayload,
3
+ recordSegment,
4
+ segmentPrefetchCovered,
5
+ takeSegment,
6
+ } from './segment-cache';
7
+ import { getStaleTimeMs } from '../segment/vary-key';
8
+
9
+ const SEGMENT_PREFETCH_HEADER = 'next-router-segment-prefetch';
10
+ const APP_SHELL_PREFETCH_HEADER = 'x-pnext-app-shell';
11
+ const INDEX_SEGMENT_PATH = '/_index';
12
+ const TREE_SEGMENT_PATH = '/_tree';
13
+ const RSC_QUERY = '_rsc';
14
+ const SEGMENT_CACHE_LIMIT = 64;
15
+
16
+ type Priority = 'low' | 'high';
17
+
18
+ interface SegmentEntry {
19
+ time: number;
20
+ staleTimeMs: number;
21
+ }
22
+
23
+ interface ScheduledPrefetch {
24
+ controller: AbortController;
25
+ priority: Priority;
26
+ }
27
+
28
+ const treeCache = new Map<string, SegmentEntry>();
29
+ const scheduled = new WeakMap<Element, ScheduledPrefetch>();
30
+ const watched = new WeakSet<Element>();
31
+ let observer: IntersectionObserver | undefined;
32
+ let installed = false;
33
+
34
+ export function installSegmentPrefetchRuntime(): void {
35
+ if ((!process.browser && typeof window === 'undefined') || installed) return;
36
+ installed = true;
37
+ document.addEventListener('pointerover', onIntent, true);
38
+ document.addEventListener('touchstart', onIntent, { capture: true, passive: true });
39
+ document.addEventListener('focusin', onIntent, true);
40
+ watchLinks();
41
+ }
42
+
43
+ function watchLinks(): void {
44
+ scanLinks(document.documentElement);
45
+ const mutations = new MutationObserver(records => {
46
+ for (const record of records) {
47
+ for (const node of record.addedNodes) {
48
+ if (node instanceof Element) scanLinks(node);
49
+ }
50
+ }
51
+ });
52
+ mutations.observe(document.documentElement, { childList: true, subtree: true });
53
+ }
54
+
55
+ function scanLinks(root: Element): void {
56
+ const links = [...root.querySelectorAll<HTMLAnchorElement>('a[data-pnext-link]')];
57
+ if (root.matches('a[data-pnext-link]')) links.push(root as HTMLAnchorElement);
58
+ for (const link of links) {
59
+ if (watched.has(link)) continue;
60
+ if (link.getAttribute('data-prefetch') === 'false') continue;
61
+ watched.add(link);
62
+ observer ??= new IntersectionObserver(onVisibility);
63
+ observer.observe(link);
64
+ }
65
+ }
66
+
67
+ function onVisibility(entries: IntersectionObserverEntry[]): void {
68
+ for (const entry of entries) {
69
+ const link = entry.target;
70
+ if (entry.isIntersecting) {
71
+ // Revealing a link prefetches the BODY segment too, not just the route
72
+ // tree: Next's default prefetch fetches the route's static data, and the
73
+ // shared segment cache is fed from that body response (see
74
+ // `prefetchSegment`). A tree-only reveal left the cache empty, so the
75
+ // navigation that follows had nothing cached to paint.
76
+ schedule(link, 'low');
77
+ } else {
78
+ scheduled.get(link)?.controller.abort();
79
+ scheduled.delete(link);
80
+ }
81
+ }
82
+ }
83
+
84
+ function onIntent(event: Event): void {
85
+ const link = linkFromEvent(event);
86
+ if (!link || link.getAttribute('data-prefetch') === 'false') return;
87
+ scheduled.get(link)?.controller.abort();
88
+ scheduled.delete(link);
89
+ schedule(link, 'high');
90
+ }
91
+
92
+ function schedule(link: Element, priority: Priority): void {
93
+ const href = link.getAttribute('href');
94
+ if (!href || isBotUserAgent()) return;
95
+ const url = softUrl(href);
96
+ if (!url) return;
97
+ const current = scheduled.get(link);
98
+ if (current?.priority === 'high') return;
99
+ const controller = new AbortController();
100
+ scheduled.set(link, { controller, priority });
101
+ void prefetchSegments(url, { priority, signal: controller.signal })
102
+ // Best-effort: an aborted (link left viewport / superseded) or failed
103
+ // segment prefetch must never surface as an unhandled rejection — the
104
+ // client error runtime would swap the live page for the global error.
105
+ .catch(() => undefined)
106
+ .finally(() => {
107
+ if (scheduled.get(link)?.controller === controller) scheduled.delete(link);
108
+ });
109
+ }
110
+
111
+ async function prefetchSegments(
112
+ url: URL,
113
+ options: { priority: Priority; signal: AbortSignal },
114
+ ): Promise<void> {
115
+ const staleTimeMs = await prefetchSegment(url, TREE_SEGMENT_PATH, options);
116
+ if (options.signal.aborted) return;
117
+ await prefetchSegment(url, INDEX_SEGMENT_PATH, options, staleTimeMs);
118
+ }
119
+
120
+ async function prefetchSegment(
121
+ url: URL,
122
+ segment: string,
123
+ options: { priority: Priority; signal: AbortSignal },
124
+ fallbackStaleTimeMs = 30_000,
125
+ ): Promise<number> {
126
+ // Dedupe against the VARY-AWARE segment cache, never a second URL-keyed map in
127
+ // front of it: `recordSegment` files every body response under its vary key, so
128
+ // a route whose segments read no params already has an entry serving this URL
129
+ // and re-fetching it is the network request the suites assert never happens.
130
+ // The tree response carries no vary metadata, so it keeps its own pathname map.
131
+ if (segment === TREE_SEGMENT_PATH) {
132
+ const cached = treeCache.get(url.pathname);
133
+ if (cached && Date.now() - cached.time < cached.staleTimeMs) return cached.staleTimeMs;
134
+ } else {
135
+ // A hit alone is not a dedupe: `takeSegment` also returns PAINT-only
136
+ // entries (a postponed body, a shared shell, another URL's runtime
137
+ // sample), and those still have real content to fetch. Only a
138
+ // network-free hit dedupes here. (api/router.ts's
139
+ // `segmentVaryPrefetchSatisfied` uses the weaker `prefetchSatisfied`; this
140
+ // is the App-Shell scheduler, whose shells are shared by construction, so
141
+ // the weaker rule would dedupe away the shell fetch that primes them.)
142
+ const hit = takeSegment({
143
+ pathname: url.pathname,
144
+ search: url.search,
145
+ segmentPath: INDEX_SEGMENT_PATH,
146
+ });
147
+ if (hit?.networkFree === true) return fallbackStaleTimeMs;
148
+ // …plus the one case `takeSegment` deliberately refuses to share: a PPR
149
+ // SHELL (postponed, so neither complete nor `x-nextjs-prerender`) whose
150
+ // published vary set already covers this URL. Nothing about it would differ
151
+ // if we fetched it again.
152
+ const covered = segmentPrefetchCovered({
153
+ pathname: url.pathname,
154
+ search: url.search,
155
+ segmentPath: INDEX_SEGMENT_PATH,
156
+ });
157
+ if (covered) return fallbackStaleTimeMs;
158
+ }
159
+ const response = await fetch(withRscQuery(url.href), {
160
+ headers: {
161
+ rsc: '1',
162
+ 'next-router-prefetch': '1',
163
+ [SEGMENT_PREFETCH_HEADER]: segment,
164
+ },
165
+ credentials: 'same-origin',
166
+ priority: options.priority,
167
+ signal: options.signal,
168
+ });
169
+ if (!response.ok) return fallbackStaleTimeMs;
170
+ const body = await response.text();
171
+ const staleTimeMs = getStaleTimeMs(headerStaleTimeSeconds(response));
172
+ // The RECORDED entry keeps the response's own window: a `stale: 0` /
173
+ // expireNow payload is born stale by contract, and clamping it would let a
174
+ // revalidated route serve pre-revalidation bytes for 30s. The floor exists
175
+ // to stop a prefetch LOOP, so it applies to the dedupe window only.
176
+ const rawSeconds = headerStaleTimeSeconds(response);
177
+ const recordStaleTimeMs = (Number.isFinite(rawSeconds) ? rawSeconds : 300) * 1000;
178
+ if (segment === TREE_SEGMENT_PATH) {
179
+ treeCache.set(url.pathname, { time: Date.now(), staleTimeMs });
180
+ trim(treeCache);
181
+ }
182
+ // The BODY response is the route's static stage. Recording it into the shared
183
+ // segment cache is the whole point of the prefetch: without it the payload was
184
+ // fetched, timed and thrown away, and the navigation that followed a reveal
185
+ // had nothing to paint (Segment-M2 fix-forward 3). The tree response carries no
186
+ // markup, so only the body segment is recorded.
187
+ if (segment === INDEX_SEGMENT_PATH) recordBodySegment(url, response, body, recordStaleTimeMs);
188
+ return staleTimeMs;
189
+ }
190
+
191
+ /** File one `/_index` body-prefetch response into the shared segment cache. */
192
+ function recordBodySegment(
193
+ url: URL,
194
+ response: Response,
195
+ body: string,
196
+ staleTimeMs: number,
197
+ ): void {
198
+ const decoded = decodeSegmentPayload(body, INDEX_SEGMENT_PATH);
199
+ if (!decoded) return;
200
+ // Mirrors the router's own segment-prefetch bookkeeping (api/router.ts
201
+ // recordSegmentEntry): a runtime (request-sampled) shell reports its own
202
+ // completeness, everything else is complete unless the render postponed.
203
+ const runtime = response.headers.get('x-pnext-runtime-prefetch') === '1';
204
+ const complete = runtime
205
+ ? response.headers.get('x-pnext-runtime-complete') === '1'
206
+ : response.headers.get('x-pnext-segment-postponed') !== '1';
207
+ recordSegment({
208
+ segmentPath: INDEX_SEGMENT_PATH,
209
+ pathname: url.pathname,
210
+ search: url.search,
211
+ html: decoded.html,
212
+ staleTimeMs,
213
+ complete,
214
+ static: response.headers.get('x-nextjs-prerender') === '1',
215
+ runtime,
216
+ vary: decoded.vary,
217
+ route: decoded.route,
218
+ params: decoded.params,
219
+ });
220
+ // A per-URL static prerender of a params-reading route also primes the route's
221
+ // shared App Shell (no-op unless experimental.appShells is on).
222
+ scheduleAppShellPrefetch({
223
+ pathname: url.pathname,
224
+ route: decoded.route,
225
+ vary: decoded.vary,
226
+ static: response.headers.get('x-nextjs-prerender') === '1',
227
+ complete,
228
+ runtime,
229
+ });
230
+ }
231
+
232
+ /** The response's reported staleness in seconds, NaN when it reported none. */
233
+ function headerStaleTimeSeconds(response: Response): number {
234
+ const header = response.headers.get('x-nextjs-stale-time');
235
+ return header === null ? Number.NaN : Number(header);
236
+ }
237
+
238
+ function withRscQuery(href: string, variant = ''): string {
239
+ const url = new URL(href, location.href);
240
+ if (url.searchParams.has(RSC_QUERY)) return url.href;
241
+ const hash = rscHash(url.pathname + url.search + (variant ? `#${variant}` : ''));
242
+ const separator = url.search ? '&' : '?';
243
+ return `${url.origin}${url.pathname}${url.search}${separator}${RSC_QUERY}=${hash}${url.hash}`;
244
+ }
245
+
246
+ function rscHash(input: string): string {
247
+ let hash = 0;
248
+ for (let index = 0; index < input.length; index++) {
249
+ hash = (Math.imul(31, hash) + input.charCodeAt(index)) | 0;
250
+ }
251
+ return (hash >>> 0).toString(36);
252
+ }
253
+
254
+ function linkFromEvent(event: Event): Element | null {
255
+ const target = event.target;
256
+ return target instanceof Element ? target.closest('a[data-pnext-link]') : null;
257
+ }
258
+
259
+ function softUrl(href: string): URL | null {
260
+ try {
261
+ const url = new URL(href, location.href);
262
+ return url.origin === location.origin ? url : null;
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
267
+
268
+ function isBotUserAgent(): boolean {
269
+ return /Googlebot(?!-)|Googlebot$|bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot/i.test(
270
+ navigator.userAgent,
271
+ );
272
+ }
273
+
274
+ function trim(cache: Map<string, SegmentEntry>): void {
275
+ while (cache.size > SEGMENT_CACHE_LIMIT) {
276
+ const oldest = cache.keys().next().value;
277
+ if (oldest === undefined) return;
278
+ cache.delete(oldest);
279
+ }
280
+ }
281
+
282
+ // App Shells: the second (param-stripped) prefetch.
283
+ //
284
+ // Revealing a link fires the ordinary per-segment prefetch of that exact URL. Under
285
+ // `experimental.appShells` a route whose segments DO read params gets a second request as well: the
286
+ // route's shared App Shell, rendered with `params` hanging. Its vary set comes back EMPTY, so the client
287
+ // files it at the fallback vary path and a navigation to any other param of the same route paints it
288
+ // instantly while the per-URL response streams in.
289
+ //
290
+ // Only ever fired for a route the server answered with a COMPLETE STATIC per-URL prerender: a
291
+ // runtime-prefetch route's own body prefetch already IS the shared shell, and a param-independent
292
+ // response (empty vary set) already serves every URL.
293
+
294
+ /** Routes whose shared shell has been requested; one request per route, ever. */
295
+ const appShellRequested = new Set<string>();
296
+
297
+ export interface AppShellPrefetchInput {
298
+ /** The URL whose per-segment prefetch just landed. */
299
+ pathname: string;
300
+ /** Route pattern the response reported (colon form), or null. */
301
+ route: string | null;
302
+ /** The response's vary set, or null when it published none. */
303
+ vary: readonly string[] | null;
304
+ static: boolean;
305
+ complete: boolean;
306
+ runtime: boolean;
307
+ }
308
+
309
+ export function scheduleAppShellPrefetch(input: AppShellPrefetchInput): void {
310
+ if (!appShellsEnabled()) return;
311
+ if (input.runtime || !input.static || !input.complete) return;
312
+ const route = input.route;
313
+ // A route with no dynamic segment has nothing to share across params, and a
314
+ // response that read no params is already the shared entry.
315
+ if (!route?.includes(':')) return;
316
+ if (!input.vary || input.vary.length === 0) return;
317
+ if (appShellRequested.has(route)) return;
318
+ appShellRequested.add(route);
319
+ void fetchAppShell(input.pathname).catch(() => {
320
+ // Best-effort: a failed shell prefetch just means the next navigation to an
321
+ // unprefetched param of this route waits for its own response.
322
+ appShellRequested.delete(route);
323
+ });
324
+ }
325
+
326
+ async function fetchAppShell(pathname: string): Promise<void> {
327
+ const response = await fetch(withRscQuery(pathname, 'app-shell'), {
328
+ headers: {
329
+ rsc: '1',
330
+ 'next-router-prefetch': '1',
331
+ [SEGMENT_PREFETCH_HEADER]: INDEX_SEGMENT_PATH,
332
+ [APP_SHELL_PREFETCH_HEADER]: '1',
333
+ },
334
+ credentials: 'same-origin',
335
+ priority: 'low',
336
+ });
337
+ if (!response.ok) return;
338
+ const decoded = decodeSegmentPayload(await response.text(), INDEX_SEGMENT_PATH);
339
+ if (!decoded) return;
340
+ recordSegment({
341
+ segmentPath: INDEX_SEGMENT_PATH,
342
+ pathname,
343
+ search: '',
344
+ html: decoded.html,
345
+ // Recorded with the response's own window (NaN → the old 300s fallback):
346
+ // the 30s floor is a re-prefetch dedupe rule, not a payload lifetime.
347
+ staleTimeMs: (seconds => (Number.isFinite(seconds) ? seconds : 300) * 1000)(
348
+ headerStaleTimeSeconds(response),
349
+ ),
350
+ // A shell, by construction: the params-dependent subtree is still a hole,
351
+ // so a navigation paints it and then fetches the per-URL remainder.
352
+ complete: false,
353
+ static: response.headers.get('x-nextjs-prerender') === '1',
354
+ runtime: false,
355
+ vary: decoded.vary,
356
+ route: decoded.route,
357
+ params: decoded.params,
358
+ });
359
+ }
360
+
361
+ function appShellsEnabled(): boolean {
362
+ if (!process.browser && typeof window === 'undefined') return false;
363
+ return (window as { __PNEXT_APP_SHELLS__?: boolean }).__PNEXT_APP_SHELLS__ === true;
364
+ }
365
+
366
+ /** Test seam: forget which routes have already primed their shared shell. */
367
+ export function resetAppShellPrefetches(): void {
368
+ appShellRequested.clear();
369
+ }
370
+
371
+ /** Drop all prefetch dedupe state — a revalidation invalidated every window. */
372
+ export function resetSegmentPrefetchDedupe(): void {
373
+ treeCache.clear();
374
+ appShellRequested.clear();
375
+ }
@@ -0,0 +1,24 @@
1
+ // Client trailing-slash helper (COMPAT - ships to the browser). Mirrors compat/client/base-path.ts.
2
+ //
3
+ // The server injects `window.__PNEXT_SKIP_TRAILING_SLASH__` when `next.config.skipTrailingSlashRedirect`
4
+ // is set; the server render path reads the globalThis global set when next.config is loaded. With the
5
+ // flag on, Link hrefs and client navigation preserve the author's exact trailing slash instead of
6
+ // normalizing it. A pure-core app never sets the global, so hrefs go through the usual normalization.
7
+
8
+ declare global {
9
+ interface Window {
10
+ __PNEXT_SKIP_TRAILING_SLASH__?: boolean;
11
+ }
12
+ var __PNEXT_SKIP_TRAILING_SLASH__: boolean | undefined;
13
+ }
14
+
15
+ /**
16
+ * True when `skipTrailingSlashRedirect` is configured. Isomorphic: the client
17
+ * reads the `window` global the render layer injects; the server reads the
18
+ * `globalThis` global set when next.config loads. Neither branch imports the
19
+ * esbuild-backed config loader, so this stays safe to ship to the browser.
20
+ */
21
+ export function skipTrailingSlashRedirect(): boolean {
22
+ if (process.browser || typeof window !== 'undefined') return window.__PNEXT_SKIP_TRAILING_SLASH__ === true;
23
+ return globalThis.__PNEXT_SKIP_TRAILING_SLASH__ === true;
24
+ }
@@ -0,0 +1,254 @@
1
+ // CSS chunking (COMPAT - Next `experimental.cssChunking`).
2
+ //
3
+ // A faithful port of Next's webpack CssChunkingPlugin (strict variant). Next merges a route's ordered
4
+ // CSS modules into as few <link> requests as possible without ever reordering a stylesheet relative to
5
+ // any other route that shares it, splitting again when a chunk would grow past MAX_CSS_CHUNK_SIZE or
6
+ // when a global stylesheet would leak into a route that does not import it.
7
+ //
8
+ // pnext ships one mode-blind build, so the STRICT algorithm runs: it produces the request counts the
9
+ // css-order suite asserts for conflict routes - which only exercise the strict expectation - while
10
+ // leaving the non-conflict routes at the same count strict and loose both expect.
11
+ //
12
+ // The result crosses the core boundary as `Map<routeId, string[][]>`: each inner array is one chunk's
13
+ // ordered slice of that route's `cssImports`, and the slices partition the route's imports in order.
14
+ // The output is per-route, which is invisible to the suite: it only checks each page's <link> count and
15
+ // computed cascade order, never cross-page asset identity.
16
+
17
+ import { statSync } from 'node:fs';
18
+ import { getNextConfig } from '../next/config-loader';
19
+
20
+ // Merge chunks until they pass this size; avoid merging past MAX. Mirrors Next.
21
+ const MIN_CSS_CHUNK_SIZE = 30 * 1024;
22
+ const MAX_CSS_CHUNK_SIZE = 100 * 1024;
23
+
24
+ interface RouteCss {
25
+ id: string;
26
+ cssImports: string[];
27
+ }
28
+
29
+ function isGlobalCss(file: string) {
30
+ return !/\.module\.(?:css|scss|sass)$/.test(file);
31
+ }
32
+
33
+ function moduleSize(file: string) {
34
+ try {
35
+ return statSync(file).size;
36
+ } catch {
37
+ return 0;
38
+ }
39
+ }
40
+
41
+ // Partition every route's ordered CSS imports into shared chunks, then project
42
+ // the global grouping back onto each route as ordered contiguous slices.
43
+ export function nextPlanRouteCssChunks(routes: RouteCss[]): Map<string, string[][]> {
44
+ if (nextCssChunkingMode() === 'loose') return loosePlanRouteCssChunks(routes);
45
+
46
+ const sizes = new Map<string, number>();
47
+ const size = (file: string) => {
48
+ let value = sizes.get(file);
49
+ if (value === undefined) {
50
+ value = moduleSize(file);
51
+ sizes.set(file, value);
52
+ }
53
+ return value;
54
+ };
55
+
56
+ interface ChunkState {
57
+ modules: string[];
58
+ requests: number;
59
+ }
60
+
61
+ const chunkStates: ChunkState[] = [];
62
+ // file -> (chunkState -> index within that route's ordered import list)
63
+ const statesByModule = new Map<string, Map<ChunkState, number>>();
64
+ for (const route of routes) {
65
+ if (route.cssImports.length === 0) continue;
66
+ const state: ChunkState = { modules: route.cssImports, requests: route.cssImports.length };
67
+ chunkStates.push(state);
68
+ route.cssImports.forEach((file, index) => {
69
+ let byState = statesByModule.get(file);
70
+ if (!byState) {
71
+ byState = new Map();
72
+ statesByModule.set(file, byState);
73
+ }
74
+ // A file may repeat within a route; the last index wins (matches Next,
75
+ // where a module maps to a single position per chunk group).
76
+ byState.set(state, index);
77
+ });
78
+ }
79
+
80
+ // Process modules by ascending index-sum so shared prefixes seed chunks first.
81
+ const ordered = [...statesByModule.keys()].sort((a, b) => {
82
+ const sum = (file: string) => {
83
+ let total = 0;
84
+ for (const index of statesByModule.get(file)!.values()) total += index;
85
+ return total;
86
+ };
87
+ return sum(a) - sum(b) || (a < b ? -1 : 1);
88
+ });
89
+ const remaining = new Set(ordered);
90
+
91
+ const chunkOfModule = new Map<string, number>();
92
+ let nextChunkId = 0;
93
+
94
+ for (const startModule of remaining) {
95
+ let globalMode = isGlobalCss(startModule);
96
+ const claimed = new Map(statesByModule.get(startModule));
97
+ const chunkModules = new Set([startModule]);
98
+ let currentSize = size(startModule);
99
+
100
+ const potential = new Map<string, Map<ChunkState, number>>();
101
+ const enqueueNext = (state: ChunkState, index: number) => {
102
+ const next = state.modules[index + 1];
103
+ if (next && remaining.has(next) && !chunkModules.has(next)) {
104
+ potential.set(next, statesByModule.get(next)!);
105
+ }
106
+ };
107
+ for (const [state, index] of claimed) enqueueNext(state, index);
108
+
109
+ let progressed = true;
110
+ while (progressed) {
111
+ progressed = false;
112
+
113
+ // Prefer the candidate that collapses the highest-request chunk group.
114
+ const candidates = [...potential.entries()].map(([file, states]) => {
115
+ let maxRequests = 0;
116
+ for (const state of states.keys()) {
117
+ if (claimed.has(state)) maxRequests = Math.max(maxRequests, state.requests);
118
+ }
119
+ return { file, states, maxRequests };
120
+ });
121
+ candidates.sort((a, b) => b.maxRequests - a.maxRequests || (a.file < b.file ? -1 : 1));
122
+
123
+ for (const { file, states } of candidates) {
124
+ const fileSize = size(file);
125
+ if (currentSize + fileSize > MAX_CSS_CHUNK_SIZE) continue;
126
+
127
+ // Strict order preservation: in every route sharing this module its
128
+ // position must be exactly one past what we've claimed there, otherwise
129
+ // merging would reorder or interleave stylesheets.
130
+ let orderOk = true;
131
+ for (const [state, index] of states) {
132
+ const claimedIndex = claimed.get(state);
133
+ if (claimedIndex === undefined) {
134
+ // A route that doesn't yet overlap: only pull it in while the chunk
135
+ // is still under the min-size target (Next's merge-up-to-MIN rule).
136
+ if (currentSize >= MIN_CSS_CHUNK_SIZE) {
137
+ orderOk = false;
138
+ break;
139
+ }
140
+ } else if (claimedIndex + 1 !== index) {
141
+ orderOk = false;
142
+ break;
143
+ }
144
+ }
145
+ if (!orderOk) continue;
146
+
147
+ // A global stylesheet must never enter a route that doesn't import it,
148
+ // and once the chunk carries a global it can't spread to new routes.
149
+ const nextIsGlobal = isGlobalCss(file);
150
+ if (globalMode) {
151
+ let leaks = false;
152
+ for (const state of states.keys()) {
153
+ if (!claimed.has(state)) {
154
+ leaks = true;
155
+ break;
156
+ }
157
+ }
158
+ if (leaks) continue;
159
+ }
160
+ if (nextIsGlobal) {
161
+ let leaks = false;
162
+ for (const state of claimed.keys()) {
163
+ if (!states.has(state)) {
164
+ leaks = true;
165
+ break;
166
+ }
167
+ }
168
+ if (leaks) continue;
169
+ }
170
+
171
+ potential.delete(file);
172
+ currentSize += fileSize;
173
+ if (nextIsGlobal) globalMode = true;
174
+ for (const [state, index] of states) {
175
+ if (claimed.has(state)) state.requests--;
176
+ claimed.set(state, index);
177
+ enqueueNext(state, index);
178
+ }
179
+ chunkModules.add(file);
180
+ progressed = true;
181
+ break;
182
+ }
183
+ }
184
+
185
+ const chunkId = nextChunkId++;
186
+ for (const file of chunkModules) {
187
+ remaining.delete(file);
188
+ chunkOfModule.set(file, chunkId);
189
+ }
190
+ }
191
+
192
+ // Project the global grouping back to per-route ordered slices. A chunk's
193
+ // modules are contiguous in every route (guaranteed by the strict merge), so
194
+ // each route's imports break cleanly at chunk-id boundaries.
195
+ const plan = new Map<string, string[][]>();
196
+ for (const route of routes) {
197
+ if (route.cssImports.length < 2) continue;
198
+ const segments: string[][] = [];
199
+ let current: string[] = [];
200
+ let currentChunk = -1;
201
+ for (const file of route.cssImports) {
202
+ const chunkId = chunkOfModule.get(file) ?? -1;
203
+ if (chunkId !== currentChunk && current.length > 0) {
204
+ segments.push(current);
205
+ current = [];
206
+ }
207
+ current.push(file);
208
+ currentChunk = chunkId;
209
+ }
210
+ if (current.length > 0) segments.push(current);
211
+ // Single segment means core's default single-chunk path already suffices.
212
+ if (segments.length > 1) plan.set(route.id, segments);
213
+ }
214
+ return plan;
215
+ }
216
+
217
+ function nextCssChunkingMode(): 'loose' | 'strict' {
218
+ const experimental = getNextConfig().experimental as { cssChunking?: unknown } | undefined;
219
+ return experimental?.cssChunking === 'loose' || experimental?.cssChunking === undefined
220
+ ? 'loose'
221
+ : 'strict';
222
+ }
223
+
224
+ // Loose mode minimizes each route's stylesheet requests independently. A
225
+ // global stylesheet begins a fresh chunk so it cannot move ahead of styles
226
+ // imported before it; subsequent modules may share that chunk.
227
+ function loosePlanRouteCssChunks(routes: RouteCss[]): Map<string, string[][]> {
228
+ const plan = new Map<string, string[][]>();
229
+
230
+ for (const route of routes) {
231
+ if (route.cssImports.length < 2) continue;
232
+
233
+ const segments: string[][] = [];
234
+ let current: string[] = [];
235
+ let currentSize = 0;
236
+ for (const file of route.cssImports) {
237
+ const size = moduleSize(file);
238
+ if (
239
+ current.length > 0 &&
240
+ (isGlobalCss(file) || currentSize + size > MAX_CSS_CHUNK_SIZE)
241
+ ) {
242
+ segments.push(current);
243
+ current = [];
244
+ currentSize = 0;
245
+ }
246
+ current.push(file);
247
+ currentSize += size;
248
+ }
249
+ if (current.length > 0) segments.push(current);
250
+ if (segments.length > 1) plan.set(route.id, segments);
251
+ }
252
+
253
+ return plan;
254
+ }