@wular/pnext 0.1.7 → 0.1.9

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 (172) hide show
  1. package/dist/server/_chunks/{aliases-j47a10ce.js → aliases-fhv4dt06.js} +7 -5
  2. package/dist/server/_chunks/{analyze-arswqe6s.js → analyze-09kx9q6r.js} +17 -13
  3. package/dist/server/_chunks/{boot-af0mv3dy.js → boot-8aj7efqk.js} +13 -11
  4. package/dist/server/_chunks/{build-spy3dxp4.js → build-jda155c9.js} +7 -5
  5. package/dist/server/_chunks/{build-tier-m7yvqz61.js → build-tier-qrkpfsk1.js} +59 -75
  6. package/dist/server/_chunks/{build-t9h0hec3.js → build-wspv5h60.js} +1004 -1191
  7. package/dist/server/_chunks/{compile-53j02j27.js → compile-040r79gj.js} +2 -2
  8. package/dist/server/_chunks/{config-n2z7yxps.js → config-7h84p6q8.js} +6 -4
  9. package/dist/server/_chunks/{config-1yzwexq6.js → config-d4hqyygy.js} +12 -3
  10. package/dist/server/_chunks/{create-meec9hs6.js → create-skj8wzt5.js} +4 -3
  11. package/dist/server/_chunks/{dev-fjmqg6en.js → dev-pennd36n.js} +2 -2
  12. package/dist/server/_chunks/{generate-n9sh6hzt.js → generate-d8vb1d21.js} +5 -4
  13. package/dist/server/_chunks/{index-eeaanp2e.js → index-0qt2vapb.js} +3 -3
  14. package/dist/server/_chunks/{index-91x12ykq.js → index-0r5eqfc7.js} +6 -1
  15. package/dist/server/_chunks/{index-9ych9ma8.js → index-0wm2p3ks.js} +2 -2
  16. package/dist/server/_chunks/{index-357na4a8.js → index-141vcwk6.js} +1 -1
  17. package/dist/server/_chunks/{index-2dv4y4wd.js → index-1y0cesgn.js} +16 -10
  18. package/dist/server/_chunks/{index-ytjvpbm2.js → index-26gwrm07.js} +3 -3
  19. package/dist/server/_chunks/{index-8k7pfejn.js → index-3r6qpe8a.js} +7 -7
  20. package/dist/server/_chunks/{index-rn8dyr1f.js → index-42fef2rv.js} +8 -48
  21. package/dist/server/_chunks/{index-ncnes8c6.js → index-47gx7trn.js} +2 -2
  22. package/dist/server/_chunks/{index-20ppawv7.js → index-4qsce64w.js} +3 -3
  23. package/dist/server/_chunks/{index-1011cscr.js → index-4vf121ws.js} +61 -29
  24. package/dist/server/_chunks/{index-cf4rvxv6.js → index-52k54ykj.js} +1 -1
  25. package/dist/server/_chunks/{index-nyrba2kr.js → index-53fnn598.js} +11 -11
  26. package/dist/server/_chunks/{index-4hgjrqh7.js → index-6tb725m8.js} +2 -2
  27. package/dist/server/_chunks/{index-3xbr4eyv.js → index-912z4mfx.js} +3 -3
  28. package/dist/server/_chunks/{index-sjpckxqn.js → index-9f49w76j.js} +1 -1
  29. package/dist/server/_chunks/{index-jepjvcrf.js → index-aarf1brn.js} +1 -1
  30. package/dist/server/_chunks/{index-9n2914ww.js → index-akkrvjn6.js} +1 -1
  31. package/dist/server/_chunks/index-bzd2ef7t.js +10 -0
  32. package/dist/server/_chunks/{index-sv8m6789.js → index-cqcwwxfp.js} +5 -5
  33. package/dist/server/_chunks/{index-yz54pxrh.js → index-d1jsmhhk.js} +4 -9
  34. package/dist/server/_chunks/{index-vvmmknm1.js → index-da0fphj0.js} +19 -15
  35. package/dist/server/_chunks/{index-sctcgyvv.js → index-drwv61cc.js} +2 -2
  36. package/dist/server/_chunks/{index-z84ab1w0.js → index-e176wk6x.js} +3 -3
  37. package/dist/server/_chunks/{index-9hx2641q.js → index-entm8bmz.js} +1 -1
  38. package/dist/server/_chunks/index-fpghsgb4.js +352 -0
  39. package/dist/server/_chunks/{index-jx7bqvw1.js → index-hr63fvzn.js} +1 -1
  40. package/dist/server/_chunks/{index-eajaspjk.js → index-j0m1jxqa.js} +1 -1
  41. package/dist/server/_chunks/index-jbj7ehef.js +50 -0
  42. package/dist/server/_chunks/{index-egykcqg3.js → index-jexsy9a8.js} +1 -1
  43. package/dist/server/_chunks/{index-fkp000p1.js → index-jjppzw0s.js} +2 -2
  44. package/dist/server/_chunks/index-jt8e1pje.js +12 -0
  45. package/dist/server/_chunks/{index-ggk2j965.js → index-kpdph69z.js} +56 -50
  46. package/dist/server/_chunks/{index-6e4fqv30.js → index-m63czgq5.js} +2 -0
  47. package/dist/server/_chunks/{index-c0g2wf3z.js → index-md5jchzy.js} +1 -1
  48. package/dist/server/_chunks/{index-tvqkq6e6.js → index-n814hcky.js} +1 -1
  49. package/dist/server/_chunks/{index-06nyqntx.js → index-q3r9z8mq.js} +1 -1
  50. package/dist/server/_chunks/{index-4q2gbfwk.js → index-qjrh7w2g.js} +2 -2
  51. package/dist/server/_chunks/{index-3b3at80h.js → index-s61bx2aj.js} +6 -3
  52. package/dist/server/_chunks/{index-ym67zqxp.js → index-tf4j19ge.js} +1 -1
  53. package/dist/server/_chunks/{index-qn4z5s3c.js → index-typcbcsv.js} +3 -3
  54. package/dist/server/_chunks/{index-p77b4kcx.js → index-veycp46e.js} +1 -1
  55. package/dist/server/_chunks/{index-11sfk4hb.js → index-vmr35jjw.js} +1 -1
  56. package/dist/server/_chunks/{index-kq8kpzye.js → index-vqs3rebe.js} +3 -3
  57. package/dist/server/_chunks/{index-danncfhy.js → index-wm36rj4v.js} +1 -1
  58. package/dist/server/_chunks/{index-z4z9zv3b.js → index-x9c55pzy.js} +141 -3
  59. package/dist/server/_chunks/{index-y2jr2n72.js → index-xrsg0041.js} +4 -4
  60. package/dist/server/_chunks/{index-k1g3epw0.js → index-xs3411qh.js} +10 -10
  61. package/dist/server/_chunks/{index-0374j2b9.js → index-y4d02znn.js} +1 -1
  62. package/dist/server/_chunks/{index-wpfhfbs4.js → index-y71a76dn.js} +4 -4
  63. package/dist/server/_chunks/{index-qm6kmhqs.js → index-y7mzvq2q.js} +1 -1
  64. package/dist/server/_chunks/{index-hkdfj58q.js → index-y9bygr95.js} +1 -1
  65. package/dist/server/_chunks/{index-3tf1xjsp.js → index-yghskk8w.js} +11 -11
  66. package/dist/server/_chunks/{index-5gyr4emz.js → index-yzcpr3yw.js} +4 -4
  67. package/dist/server/_chunks/{index-x93hv0sc.js → index-zedjt25v.js} +23 -10
  68. package/dist/server/_chunks/{index-tq0pc7eg.js → index-zfrwq472.js} +3 -3
  69. package/dist/server/_chunks/{loader-7yw0j98z.js → loader-w1e0n5sm.js} +12 -10
  70. package/dist/server/_chunks/{modules-jn8bmbn8.js → modules-730j14cm.js} +14 -12
  71. package/dist/server/_chunks/{pipeline-yy9jhydf.js → pipeline-yjdnwbhm.js} +25 -21
  72. package/dist/server/_chunks/{production-jar9ztm3.js → production-1xe9b7wd.js} +14 -3
  73. package/dist/server/_chunks/{proxy-config-j84dgksn.js → proxy-config-9r9e0jvn.js} +7 -5
  74. package/dist/server/_chunks/{proxy-w1d0wmsn.js → proxy-pnevsbfd.js} +12 -10
  75. package/dist/server/_chunks/{router-k1reahfn.js → router-rzge0vz8.js} +10 -7
  76. package/dist/server/_chunks/{routes-2s277fc6.js → routes-0x8wjvcj.js} +12 -10
  77. package/dist/server/_chunks/{run-pvd8bs1t.js → run-tk90r3ac.js} +5 -4
  78. package/dist/server/_chunks/{server-mt4a994c.js → server-gev78ejc.js} +38 -34
  79. package/dist/server/_chunks/{tsconfig-defaults-1nb3308g.js → tsconfig-defaults-qe9wd6ne.js} +5 -4
  80. package/dist/server/_chunks/{typegen-0qr0459b.js → typegen-0yw8sry8.js} +15 -13
  81. package/dist/server/cli/index.js +12 -15
  82. package/dist/server/cli/start.js +35 -14
  83. package/dist/server/compat/next/cache.js +5 -5
  84. package/dist/server/compat/next/config-headers.js +8 -6
  85. package/dist/server/compat/next/config-loader.js +1 -1
  86. package/dist/server/compat/next/dist/server/web/spec-extension/revalidate.js +5 -5
  87. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-cache.js +5 -5
  88. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-no-store.js +5 -5
  89. package/dist/server/compat/next/dynamic.js +3 -14
  90. package/dist/server/compat/next/font/cache.js +6 -4
  91. package/dist/server/compat/next/font/local.js +7 -5
  92. package/dist/server/compat/next/font/runtime.js +7 -5
  93. package/dist/server/compat/next/headers.js +4 -4
  94. package/dist/server/compat/next/image/config.js +2 -2
  95. package/dist/server/compat/next/image/optimizer.js +3 -3
  96. package/dist/server/compat/next/image/static-metadata.js +4 -4
  97. package/dist/server/compat/next/legacy-request-apis.js +3 -3
  98. package/dist/server/compat/next/link-transform.js +4 -3
  99. package/dist/server/compat/next/navigation.js +5 -4
  100. package/dist/server/compat/next/redirects.js +8 -6
  101. package/dist/server/compat/next/resource-hints.js +2 -2
  102. package/dist/server/compat/next/rewrites.js +7 -5
  103. package/dist/server/compat/next/root-params.js +3 -3
  104. package/dist/server/compat/next/route-state.js +2 -2
  105. package/dist/server/compat/next/script.js +2 -2
  106. package/dist/server/compat/next/server.js +5 -5
  107. package/dist/server/generation.json +1 -1
  108. package/package.json +1 -1
  109. package/reference/compat.md +1 -1
  110. package/reference/config.md +2 -2
  111. package/reference/dev.md +1 -1
  112. package/reference/getting-started.md +1 -1
  113. package/reference/rendering.md +4 -4
  114. package/src/api/dynamic.tsx +84 -4
  115. package/src/cli/adapters/vercel-warm.ts +40 -29
  116. package/src/cli/adapters/vercel.ts +101 -1265
  117. package/src/cli/analyze.ts +3 -2
  118. package/src/cli/build/cache.ts +6 -6
  119. package/src/cli/build.ts +33 -33
  120. package/src/cli/index.ts +4 -6
  121. package/src/cli/serve/pipeline.ts +16 -10
  122. package/src/cli/serve/standalone-worker.ts +8 -0
  123. package/src/cli/serve/standalone.ts +727 -0
  124. package/src/cli/start.ts +29 -8
  125. package/src/client/build.ts +1 -1
  126. package/src/client/reference-stub.ts +2 -34
  127. package/src/compat/actions/extensions.ts +3 -2
  128. package/src/compat/bundler/require-context.ts +2 -1
  129. package/src/compat/bundler/webpack-loaders.ts +3 -2
  130. package/src/compat/bundler/worker.ts +2 -2
  131. package/src/compat/css/inline-css.ts +1 -1
  132. package/src/compat/export/build-complete.ts +1 -1
  133. package/src/compat/export/emit.ts +4 -4
  134. package/src/compat/export/standalone.ts +30 -65
  135. package/src/compat/image-optimizer/optimize.ts +1 -1
  136. package/src/compat/lifecycle/instrumentation-client.ts +4 -3
  137. package/src/compat/metadata-artifacts.ts +1 -1
  138. package/src/compat/next/config-loader.ts +3 -0
  139. package/src/compat/next/dynamic.tsx +10 -23
  140. package/src/compat/next/font/cache.ts +2 -1
  141. package/src/compat/next/font/runtime.ts +1 -1
  142. package/src/compat/register/image.ts +1 -1
  143. package/src/compat/typecheck/check.ts +6 -4
  144. package/src/config.ts +28 -5
  145. package/src/css/assets.ts +1 -1
  146. package/src/css/build.ts +16 -9
  147. package/src/dev/client-chunk-store.ts +2 -1
  148. package/src/dev/restart/client-key.ts +2 -1
  149. package/src/dev/restart/global-css.ts +2 -1
  150. package/src/dev/restart/route-bundle-key.ts +2 -1
  151. package/src/dev/restart/route-facts.ts +2 -1
  152. package/src/dev/server.ts +8 -7
  153. package/src/out-paths.ts +10 -0
  154. package/src/render/renderer.ts +1 -1
  155. package/src/resolve/dynamic.ts +4 -0
  156. package/src/resolve/engine.ts +2 -12
  157. package/src/resolve/scan-facts.ts +185 -1
  158. package/src/routing/metadata-files.ts +1 -1
  159. package/src/runtime/loader.ts +5 -3
  160. package/src/runtime/module-cache.ts +2 -1
  161. package/src/runtime/modules.ts +50 -16
  162. package/src/runtime/prebundle.ts +23 -3
  163. package/src/runtime/production.ts +105 -40
  164. package/src/runtime/release-graph.ts +21 -13
  165. package/src/runtime/vendor-build.ts +45 -23
  166. package/src/types.ts +2 -3
  167. package/src/utils/directive.ts +40 -0
  168. package/src/utils/fs.ts +7 -2
  169. package/src/utils/resource-query.ts +12 -0
  170. package/dist/server/_chunks/index-tqn2qnnn.js +0 -241
  171. package/dist/server/_chunks/index-y36a0ybw.js +0 -57
  172. package/src/cli/serve/entry.ts +0 -52
@@ -6,6 +6,7 @@
6
6
  * imports them; this module never imports from `./vendor-build`.
7
7
  */
8
8
 
9
+ import { hasModuleDirective } from '../utils/directive'
9
10
  import { createHash } from 'node:crypto'
10
11
  import { createRequire } from 'node:module'
11
12
  import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'
@@ -33,6 +34,7 @@ import {
33
34
  pnextAliases,
34
35
  type CompatAliasTarget,
35
36
  type ResolvedConfig,
37
+ outCachePath,
36
38
  } from '../config'
37
39
  import {
38
40
  rewriteDynamicCallTargets,
@@ -175,7 +177,7 @@ export function registerServerRuntime(config: ResolvedConfig, sourceFiles: strin
175
177
  ...rootPaths(config.root),
176
178
  ...rootPaths(config.appPath),
177
179
  // A release build evaluates its compiled artifacts as the release will: natively.
178
- ...(isReleaseCompile() ? [] : rootPaths(path.join(config.outPath, 'cache', 'server'))),
180
+ ...(isReleaseCompile() ? [] : rootPaths(path.join(outCachePath(config.outPath), 'server'))),
179
181
  // The pnext src root, so the framework's own source transforms too.
180
182
  ...rootPaths(path.join(import.meta.dirname, '..')),
181
183
  ...sourceRoots.flatMap(rootPaths),
@@ -380,7 +382,7 @@ export function nodeModuleSourceLoadsAsIs(source: string) {
380
382
  /** The relative specifiers of a file that loads as-is, or undefined when it needs the bundle. */
381
383
  function nodeModuleRelativeImports(source: string) {
382
384
  if (serverDefineOptions().define || source.includes('use cache')) return undefined
383
- if (/^\s*(?:(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)\s*)*['"]use (?:client|server)['"]/.test(source)) {
385
+ if (hasModuleDirective(source, 'use client') || hasModuleDirective(source, 'use server')) {
384
386
  return undefined
385
387
  }
386
388
  try {
@@ -1140,7 +1142,7 @@ async function staticImageModuleSource(config: ResolvedConfig | undefined, file:
1140
1142
  const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit)
1141
1143
  if (config) {
1142
1144
  for (const asset of emitted) {
1143
- const target = path.join(config.outPath, 'public', ...asset.relative.split('/'))
1145
+ const target = path.join(config.outPath, 'static', ...asset.relative.split('/'))
1144
1146
  await mkdir(path.dirname(target), { recursive: true })
1145
1147
  if (!existsSync(target)) await Bun.write(target, asset.bytes)
1146
1148
  }
@@ -17,6 +17,7 @@ import { fileURLToPath } from 'node:url'
17
17
  import type { ResolvedConfig } from '../config'
18
18
  import { writeFileAtomic } from '../utils/fs'
19
19
  import { traceEnabled } from '../utils/trace-flags'
20
+ import { outCachePath } from '../out-paths'
20
21
 
21
22
  /**
22
23
  * Kill switch for the request-head trims: the naming walk's synchronous source
@@ -205,7 +206,7 @@ export function devModuleCache(
205
206
  }
206
207
 
207
208
  export function cacheRoot(outPath: string) {
208
- return path.join(outPath, 'cache', 'server')
209
+ return path.join(outCachePath(outPath), 'server')
209
210
  }
210
211
 
211
212
  function createDevModuleCache(
@@ -61,7 +61,9 @@ import {
61
61
  resolvePackageSpecifier,
62
62
  } from '../resolve/imports'
63
63
  import {
64
+ absolutePathLiterals,
64
65
  importSpecifiers,
66
+ lineComments,
65
67
  moduleSpecifierEdges,
66
68
  rewriteSpecifierLiterals,
67
69
  stripJsonImportAttributes,
@@ -1498,16 +1500,38 @@ function relocateSource(
1498
1500
  const specifiers = spliceSource(contents, edits)
1499
1501
  if (!isReleaseCompile()) return specifiers
1500
1502
  // Path literals (client-reference targets, module URLs) resolve beside the artifact at runtime.
1501
- return specifiers.replace(
1502
- /(?<!\\)"((?:file:\/\/)?\/[^"\\\n]+)"(\s*:)?/g,
1503
- (literal, value: string, key: string | undefined) => {
1504
- const url = value.startsWith('file:')
1505
- const target = locate(url ? fileURLToPath(value) : value)
1506
- if (!target) return literal
1507
- const href = `new URL(${JSON.stringify(relativeTo(target))}, import.meta.url)`
1508
- const expression = url ? `${href}.href` : `decodeURIComponent(${href}.pathname)`
1509
- return key ? `[${expression}]${key}` : expression
1510
- },
1503
+ const expression = (value: string) => {
1504
+ const url = value.startsWith('file:')
1505
+ const target = locate(url ? fileURLToPath(value) : value)
1506
+ if (!target) return undefined
1507
+ const href = `new URL(${JSON.stringify(relativeTo(target))}, import.meta.url)`
1508
+ return url ? `${href}.href` : `decodeURIComponent(${href}.pathname)`
1509
+ }
1510
+ const literals = absolutePathLiterals(file, specifiers)
1511
+ if (!literals) {
1512
+ // A recovered parse: the textual form, double-quoted literals only.
1513
+ return specifiers.replace(
1514
+ /(?<!\\)"((?:file:\/\/)?\/[^"\\\n]+)"(\s*:)?/g,
1515
+ (literal, value: string, key: string | undefined) => {
1516
+ const replacement = expression(value)
1517
+ if (!replacement) return literal
1518
+ return key ? `[${replacement}]${key}` : replacement
1519
+ },
1520
+ )
1521
+ }
1522
+ return spliceSource(
1523
+ specifiers,
1524
+ literals.flatMap(literal => {
1525
+ const replacement = expression(literal.value)
1526
+ if (!replacement) return []
1527
+ return [
1528
+ {
1529
+ start: literal.start,
1530
+ end: literal.end,
1531
+ value: literal.key ? `[${replacement}]` : replacement,
1532
+ },
1533
+ ]
1534
+ }),
1511
1535
  )
1512
1536
  }
1513
1537
 
@@ -1591,7 +1615,7 @@ export function releaseArtifactSource(file: string, contents: string) {
1591
1615
  if (!cacheRoot || !anchors) return contents
1592
1616
  return relocateSource(
1593
1617
  file,
1594
- releaseInputComments(contents),
1618
+ releaseInputComments(file, contents),
1595
1619
  absolute => (isInside(cacheRoot, absolute) ? absolute : portableTarget(anchors, absolute)),
1596
1620
  absolute => !isInside(cacheRoot, absolute) && isInstalled(absolute),
1597
1621
  )
@@ -1601,11 +1625,21 @@ export function releaseArtifactSource(file: string, contents: string) {
1601
1625
  * esbuild's per-input `// <path>` comments, cut to the file name (or its `node_modules/` path, which
1602
1626
  * names a vendored font caller): their paths walk the build machine.
1603
1627
  */
1604
- function releaseInputComments(contents: string) {
1605
- return contents.replace(/^\/\/ (?:[\w-]+:)?\.{0,2}\/[^\s]*$/gm, comment => {
1628
+ function releaseInputComments(file: string, contents: string) {
1629
+ const shorten = (comment: string) => {
1606
1630
  const installed = comment.indexOf('node_modules/')
1607
1631
  return `// ${installed === -1 ? path.basename(comment) : comment.slice(installed)}`
1608
- })
1632
+ }
1633
+ const inputPath = /^ (?:[\w-]+:)?\.{0,2}\/\S*$/
1634
+ const comments = lineComments(file, contents)
1635
+ // A recovered parse: the textual form, whole lines only.
1636
+ if (!comments) return contents.replace(/^\/\/ (?:[\w-]+:)?\.{0,2}\/[^\s]*$/gm, shorten)
1637
+ return spliceSource(
1638
+ contents,
1639
+ comments
1640
+ .filter(comment => inputPath.test(comment.value))
1641
+ .map(comment => ({ ...comment, value: shorten(`//${comment.value}`) })),
1642
+ )
1609
1643
  }
1610
1644
 
1611
1645
  /** `target` as a path a moved release still reaches: through the app's own tree, never the machine's. */
@@ -1634,7 +1668,7 @@ async function writeCompiledFile(file: string, contents: string) {
1634
1668
  const cacheRoot = path.dirname(profileRoot)
1635
1669
  contents = relocateSource(
1636
1670
  file,
1637
- anchors ? releaseInputComments(contents) : contents,
1671
+ anchors ? releaseInputComments(file, contents) : contents,
1638
1672
  absolute =>
1639
1673
  isInside(cacheRoot, absolute) ? absolute : anchors && portableTarget(anchors, absolute),
1640
1674
  absolute => !isInside(cacheRoot, absolute) && isInstalled(absolute),
@@ -3585,7 +3619,7 @@ async function staticImageModuleSource(config: ResolvedConfig, file: string) {
3585
3619
  const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit })
3586
3620
  const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit)
3587
3621
  for (const relative of emitted) {
3588
- const target = path.join(config.outPath, 'public', ...relative.split('/'))
3622
+ const target = path.join(config.outPath, 'static', ...relative.split('/'))
3589
3623
  await mkdir(path.dirname(target), { recursive: true })
3590
3624
  if (!existsSync(target)) await copyFile(sourcePath, target)
3591
3625
  }
@@ -55,6 +55,21 @@ export function prebundledFile(file: string): string {
55
55
  return entries.has(entry) ? path.join(distRoot, `${entry}.js`) : file
56
56
  }
57
57
 
58
+ /** A resolve-only `Bun.plugin`, typed here: these modules sit in app type graphs without bun-types. */
59
+ interface ResolvePlugin {
60
+ name: string
61
+ setup(build: {
62
+ onResolve(
63
+ options: { filter: RegExp },
64
+ resolve: (args: { path: string; importer: string }) => { path: string } | undefined,
65
+ ): void
66
+ }): void
67
+ }
68
+
69
+ export function registerResolvePlugin(plugin: ResolvePlugin) {
70
+ ;(globalThis as unknown as { Bun: { plugin(plugin: ResolvePlugin): void } }).Bun.plugin(plugin)
71
+ }
72
+
58
73
  let registered = false
59
74
 
60
75
  /** Route imports of framework source, by path, to the prebundle. */
@@ -62,11 +77,16 @@ export function registerPrebundleResolve(): void {
62
77
  if (!prebundle || registered) return
63
78
  registered = true
64
79
  const escaped = srcRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
65
- Bun.plugin({
80
+ registerResolvePlugin({
66
81
  name: 'pnext-prebundle',
67
82
  setup(build) {
68
- build.onResolve({ filter: new RegExp(`^(?:file://)?${escaped}.*\\.tsx?$`) }, args => {
69
- const file = args.path.startsWith('file:') ? fileURLToPath(args.path) : args.path
83
+ build.onResolve({ filter: new RegExp(`^${escaped}.*\\.tsx?$`) }, args => {
84
+ const target = prebundledFile(args.path)
85
+ return target === args.path ? undefined : { path: target }
86
+ })
87
+ // A URL encodes the root (`%20` for a space), so it is decoded before the root is compared.
88
+ build.onResolve({ filter: /^file:.*\.tsx?$/ }, args => {
89
+ const file = fileURLToPath(args.path)
70
90
  const target = prebundledFile(file)
71
91
  return target === file ? undefined : { path: target }
72
92
  })
@@ -3,16 +3,17 @@
3
3
  * build froze. A served request never reads, stats or hashes app source or config, and never
4
4
  * compiles; everything it imports is named here. Dev and the build itself keep their own loaders.
5
5
  */
6
- import { type Dirent, existsSync, readFileSync, readdirSync } from 'node:fs'
7
- import { mkdir, rename, rm, writeFile } from 'node:fs/promises'
6
+ import { type Dirent, existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs'
7
+ import { rename, writeFile } from 'node:fs/promises'
8
8
  import path from 'node:path'
9
9
  import { pathToFileURL } from 'node:url'
10
10
  import type { ResolvedConfig } from '../config'
11
11
  import type { ReleasedFont } from '../render/hooks'
12
12
  import type { StaticMetadataFile } from '../routing/metadata-files'
13
+ import { prebundledFile, registerResolvePlugin } from './prebundle'
14
+ import { splitResourceQuery } from '../utils/resource-query'
13
15
 
14
16
  export const BUILD_INDEX_VERSION = 1
15
- const LOCATOR_FILE = 'release-locator.json'
16
17
 
17
18
  /** `server` is the RSC/server layer, `client` the SSR copy of client components. */
18
19
  export type ReleaseLayer = 'server' | 'client'
@@ -91,6 +92,96 @@ export function buildIndexFile(outPath: string) {
91
92
  return path.join(outPath, 'server', 'build-index.json')
92
93
  }
93
94
 
95
+ export function buildManifestFile(outPath: string) {
96
+ return path.join(outPath, 'server', 'manifest.json')
97
+ }
98
+
99
+ /** The framework copy a standalone dir ships: its compiled modules name framework files there. */
100
+ export function releaseFrameworkRoot(outPath: string) {
101
+ try {
102
+ const { name } = JSON.parse(readFileSync(path.join(frameworkRoot, 'package.json'), 'utf8')) as {
103
+ name: string
104
+ }
105
+ return realpathSync(path.join(outPath, 'node_modules', name))
106
+ } catch {
107
+ return frameworkRoot
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Where a release's app sources are, as its compiled modules name them: the app root the build recorded
113
+ * relative to its output. In place that is the real root; a copied standalone keeps the same relation.
114
+ */
115
+ export function releaseSourceRoot(outPath: string, serializedOutPath: string) {
116
+ return path.resolve(outPath, path.relative(serializedOutPath, '.'))
117
+ }
118
+
119
+ /** Build-machine facts about a standalone dir, kept in the build cache beside it. */
120
+ export function standaloneOriginsFile(outPath: string) {
121
+ return path.join(path.dirname(outPath), 'cache', 'standalone.json')
122
+ }
123
+
124
+ let shippedRelease: string | null | undefined
125
+
126
+ /** The standalone dir this framework copy ships in (`<dir>/node_modules/…/<name>`), if it is one. */
127
+ export function servingReleaseDir(): string | undefined {
128
+ if (shippedRelease === undefined) {
129
+ shippedRelease = null
130
+ // Isolated installs nest the copy deeper: `node_modules/.bun/<pkg>/node_modules/<name>`.
131
+ for (
132
+ let dir = path.dirname(frameworkRoot);
133
+ dir !== path.dirname(dir);
134
+ dir = path.dirname(dir)
135
+ ) {
136
+ if (path.basename(dir) === 'node_modules' && existsSync(buildIndexFile(path.dirname(dir)))) {
137
+ shippedRelease = path.dirname(dir)
138
+ break
139
+ }
140
+ }
141
+ }
142
+ return shippedRelease ?? undefined
143
+ }
144
+
145
+ const linkedReleases = new Map<string, boolean>()
146
+
147
+ /**
148
+ * Serve `release` from this framework copy without loading the standalone one beside it: its imports
149
+ * of its `node_modules` copies resolve back to the files they were cloned from, so the process keeps
150
+ * one instance of the framework and of every package. False when this copy cannot serve it in
151
+ * place: the build cache naming the origins is gone, or the release moved since.
152
+ */
153
+ export function linkStandaloneOrigins(release: string): boolean {
154
+ if (servingReleaseDir() === release) return true
155
+ const known = linkedReleases.get(release)
156
+ if (known !== undefined) return known
157
+ let origins: Record<string, string> | undefined
158
+ try {
159
+ const parsed = JSON.parse(readFileSync(standaloneOriginsFile(release), 'utf8')) as {
160
+ release: string
161
+ origins: Record<string, string>
162
+ }
163
+ if (parsed.release === release) origins = parsed.origins
164
+ } catch {
165
+ origins = undefined
166
+ }
167
+ linkedReleases.set(release, origins !== undefined)
168
+ if (!origins) return false
169
+ const files = origins
170
+ const modules = `${path.join(release, 'node_modules')}${path.sep}`
171
+ registerResolvePlugin({
172
+ name: 'pnext-standalone-origins',
173
+ setup(build) {
174
+ build.onResolve({ filter: /node_modules\// }, args => {
175
+ const file = path.resolve(path.dirname(args.importer), splitResourceQuery(args.path).path)
176
+ if (!file.startsWith(modules)) return undefined
177
+ const origin = files[path.relative(release, file).split(path.sep).join('/')]
178
+ return origin && existsSync(origin) ? { path: prebundledFile(origin) } : undefined
179
+ })
180
+ },
181
+ })
182
+ return true
183
+ }
184
+
94
185
  export function releaseModuleKey(layer: ReleaseLayer, target: string, file: string) {
95
186
  return `${layer}:${target}:${file}`
96
187
  }
@@ -145,11 +236,12 @@ export function attachRelease(config: ResolvedConfig, index = readBuildIndex(con
145
236
  const existing = releases.get(config)
146
237
  if (existing) return existing
147
238
  const fromRoot = (file: string) => path.resolve(config.root, file)
239
+ const framework = releaseFrameworkRoot(config.outPath)
148
240
  const modules = new Map<string, string>()
149
241
  for (const [key, artifact] of Object.entries(index.modules)) {
150
242
  const second = key.indexOf(':', key.indexOf(':') + 1)
151
243
  modules.set(
152
- `${key.slice(0, second + 1)}${releaseSourceFile(key.slice(second + 1), config.root, config.workspaceRoot)}`,
244
+ `${key.slice(0, second + 1)}${releaseSourceFile(key.slice(second + 1), config.root, config.workspaceRoot, framework)}`,
153
245
  pathToFileURL(path.resolve(config.outPath, artifact)).href,
154
246
  )
155
247
  }
@@ -272,11 +364,16 @@ export function releaseSourceKey(file: string, root: string, workspaceRoot: stri
272
364
  return `r:${toPosix(inRoot)}`
273
365
  }
274
366
 
275
- export function releaseSourceFile(key: string, root: string, workspaceRoot: string) {
367
+ export function releaseSourceFile(
368
+ key: string,
369
+ root: string,
370
+ workspaceRoot: string,
371
+ framework = frameworkRoot,
372
+ ) {
276
373
  const value = key.slice(2)
277
374
  if (key.startsWith('r:')) return path.join(root, value)
278
375
  if (key.startsWith('w:')) return path.resolve(workspaceRoot, value)
279
- if (key.startsWith('f:')) return path.resolve(frameworkRoot, value)
376
+ if (key.startsWith('f:')) return path.resolve(framework, value)
280
377
  return path.join(root, value)
281
378
  }
282
379
 
@@ -303,23 +400,6 @@ export function readBuildIndex(outPath: string): BuildIndex {
303
400
  return index
304
401
  }
305
402
 
306
- /**
307
- * Where a built root keeps its release: the dir its locator names when the latest build wrote outside
308
- * the default out dir (a build into the default empties it, locator included), else the default.
309
- * Undefined when the root holds no build.
310
- */
311
- export function locateRelease(root: string, defaultOutDir: string): string | undefined {
312
- const outPath = path.join(root, defaultOutDir)
313
- try {
314
- const { outDir } = JSON.parse(readFileSync(path.join(outPath, LOCATOR_FILE), 'utf8')) as {
315
- outDir: string
316
- }
317
- return path.resolve(root, outDir)
318
- } catch {
319
- return existsSync(buildIndexFile(outPath)) ? outPath : undefined
320
- }
321
- }
322
-
323
403
  /** The resolved config as the build saw it, with paths made relative to the root. */
324
404
  export function serializeReleaseConfig(config: ResolvedConfig): Record<string, unknown> {
325
405
  const relative = (file: string) => path.relative(config.root, file) || '.'
@@ -367,25 +447,10 @@ export function releaseConfig(
367
447
  }
368
448
  }
369
449
 
370
- /** Publish the build index last, atomically, plus a locator when the out dir is not the default. */
371
- export async function writeBuildIndex(
372
- config: ResolvedConfig,
373
- index: BuildIndex,
374
- defaultOutDir: string,
375
- ) {
450
+ /** Publish the build index last, atomically. */
451
+ export async function writeBuildIndex(config: ResolvedConfig, index: BuildIndex) {
376
452
  const file = buildIndexFile(config.outPath)
377
453
  const temporary = `${file}.${process.pid}.tmp`
378
454
  await writeFile(temporary, JSON.stringify(index))
379
455
  await rename(temporary, file)
380
- const defaultOut = path.join(config.root, defaultOutDir)
381
- if (path.resolve(config.outPath) === path.resolve(defaultOut)) return
382
- await mkdir(defaultOut, { recursive: true })
383
- const locator = path.join(defaultOut, LOCATOR_FILE)
384
- await writeFile(
385
- `${locator}.${process.pid}.tmp`,
386
- JSON.stringify({ outDir: path.relative(config.root, config.outPath) }),
387
- )
388
- await rename(`${locator}.${process.pid}.tmp`, locator)
389
- // A default-out release left by an earlier build is now stale: the locator already outranks it.
390
- await rm(buildIndexFile(defaultOut), { force: true })
391
456
  }
@@ -9,7 +9,8 @@ import { readFile, readdir, rm } from 'node:fs/promises'
9
9
  import path from 'node:path'
10
10
  import type { ResolvedConfig } from '../config'
11
11
  import { getExternalPackagePolicy } from '../resolve/imports'
12
- import { importSpecifiers } from '../resolve/scan-facts'
12
+ import { importMetaRefs, importSpecifiers } from '../resolve/scan-facts'
13
+ import { splitResourceQuery } from '../utils/resource-query'
13
14
  import { toPosixPath, writeFileAtomic } from '../utils/fs'
14
15
  import { hashBundleSpecifier } from './loader'
15
16
  import { cacheRoot, devSourceIdentity } from './module-cache'
@@ -242,9 +243,7 @@ export async function pruneReleaseVendor(config: ResolvedConfig, sources: Map<st
242
243
  const cache = cacheRoot(config.outPath)
243
244
  const vendor = path.join(cache, 'vendor')
244
245
  const graphs = path.join(cache, 'deps')
245
- const compiled = (await readdir(cache, { recursive: true, withFileTypes: true }))
246
- .filter(entry => !entry.isDirectory() && /\.[cm]?js$/.test(entry.name))
247
- .map(entry => path.join(entry.parentPath, entry.name))
246
+ const compiled = await compiledFiles(cache)
248
247
  const reached = new Set<string>()
249
248
  let frontier = compiled.filter(file => !isInside(vendor, file) && !isInside(graphs, file))
250
249
  while (frontier.length > 0) {
@@ -260,7 +259,7 @@ export async function pruneReleaseVendor(config: ResolvedConfig, sources: Map<st
260
259
  (/\.[cm]?js$/.test(file)
261
260
  ? await readFile(file, 'utf8').catch(() => undefined)
262
261
  : undefined)
263
- for (const specifier of code ? relativeReferences(code) : []) {
262
+ for (const specifier of code ? relativeReferences(file, code) : []) {
264
263
  const target = path.resolve(path.dirname(file), splitSuffix(specifier).specifier)
265
264
  if (isInside(vendor, target)) next.push(target)
266
265
  }
@@ -275,14 +274,25 @@ export async function pruneReleaseVendor(config: ResolvedConfig, sources: Map<st
275
274
  )
276
275
  }
277
276
 
277
+ // The compat cache mirrors app-root entries (node_modules included) as symlinks; never follow them.
278
+ async function compiledFiles(dir: string): Promise<string[]> {
279
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
280
+ const nested = await Promise.all(
281
+ entries.map(async entry => {
282
+ const file = path.join(dir, entry.name)
283
+ if (entry.isDirectory()) return compiledFiles(file)
284
+ return entry.isFile() && /\.[cm]?js$/.test(entry.name) ? [file] : []
285
+ }),
286
+ )
287
+ return nested.flat()
288
+ }
289
+
278
290
  /** Relative module references in emitted code: imports, requires and `new URL(…, import.meta.url)`. */
279
- function relativeReferences(code: string) {
291
+ function relativeReferences(file: string, code: string) {
280
292
  return [
281
293
  ...outputSpecifiers(code).map(found => found.value),
282
294
  ...emittedRefs(code).map(ref => ref.specifier),
283
- ...[...code.matchAll(/new URL\(("[^"]+"),\s*import\.meta\.url\)/g)].map(
284
- match => JSON.parse(match[1]!) as string,
285
- ),
295
+ ...importMetaRefs(file, code).urls.map(url => url.specifier),
286
296
  ].filter(specifier => specifier.startsWith('.'))
287
297
  }
288
298
 
@@ -296,10 +306,8 @@ function entryName(config: ResolvedConfig, entry: string) {
296
306
  }
297
307
 
298
308
  function splitSuffix(value: string) {
299
- const index = value.search(/[?#]/)
300
- return index < 0
301
- ? { specifier: value, suffix: '' }
302
- : { specifier: value.slice(0, index), suffix: value.slice(index) }
309
+ const { path: specifier, query: suffix } = splitResourceQuery(value)
310
+ return { specifier, suffix }
303
311
  }
304
312
 
305
313
  function packageName(specifier: string) {
@@ -34,7 +34,12 @@ import {
34
34
  serverDefineOptions,
35
35
  type ServerEsbuildPluginOptions,
36
36
  } from '../extensions'
37
- import { pathToFileHref, type CompatAliasTarget, type ResolvedConfig } from '../config'
37
+ import {
38
+ pathToFileHref,
39
+ type CompatAliasTarget,
40
+ type ResolvedConfig,
41
+ outCachePath,
42
+ } from '../config'
38
43
  import {
39
44
  cacheRoot,
40
45
  devArtifactUsable,
@@ -65,9 +70,10 @@ import {
65
70
  } from '../client/reference-stub'
66
71
  import { noteCompiledClientReference } from '../client/reference'
67
72
  import { reactCompatEnabled } from '../render/hooks'
68
- import { writeFileAtomic } from '../utils/fs'
73
+ import { toPosixPath, writeFileAtomic } from '../utils/fs'
69
74
  import { releaseArtifactSource } from './modules'
70
75
  import { formatDuration } from '../utils/verbose'
76
+ import { hasModuleState, importMetaRefs, scanFacts } from '../resolve/scan-facts'
71
77
  import {
72
78
  addCommonJsNamedExports,
73
79
  canonicalVendorCode,
@@ -263,6 +269,21 @@ function appendReleaseLog(config: ResolvedConfig, record: object) {
263
269
  * itself (`import.meta.url`) or imports computed paths loads in place, as in 0.1.6: Next's webpack
264
270
  * contexts bundle computed imports instead. Returns the files written and the entries left in place.
265
271
  */
272
+ /**
273
+ * esbuild names CommonJS modules by path from `absWorkingDir`; a `node_modules` linked from outside
274
+ * the app makes those names climb to the build machine's absolute path. The quoted prefix to rewrite.
275
+ */
276
+ function machineModuleKeyPrefix(root: string) {
277
+ let real: string
278
+ try {
279
+ real = realpathSync(path.join(root, 'node_modules'))
280
+ } catch {
281
+ return undefined
282
+ }
283
+ const relative = toPosixPath(path.relative(root, real))
284
+ return relative.startsWith('../') ? `"${relative}/` : undefined
285
+ }
286
+
266
287
  export async function linkReleaseDependencies(
267
288
  config: ResolvedConfig,
268
289
  layer: ReleaseDependencyLayer,
@@ -338,10 +359,12 @@ export async function linkReleaseDependencies(
338
359
  await rm(staging, { recursive: true, force: true })
339
360
  try {
340
361
  await mkdir(path.join(staging, 'chunks'), { recursive: true })
362
+ const keys = machineModuleKeyPrefix(config.root)
341
363
  for (const output of outputs) {
364
+ const text = keys ? output.text.replaceAll(keys, '"node_modules/') : output.text
342
365
  const code = entryFiles.has(output.path)
343
- ? addCommonJsNamedExports(output.text, sharedCommonJsBody(output.path, output.text, texts))
344
- : output.text
366
+ ? addCommonJsNamedExports(text, sharedCommonJsBody(output.path, text, texts))
367
+ : text
345
368
  await writeFile(
346
369
  path.join(staging, path.relative(outdir, output.path)),
347
370
  releaseArtifactSource(
@@ -364,16 +387,13 @@ export async function linkReleaseDependencies(
364
387
 
365
388
  // `import.meta` paths and an `import()` of a computed path resolve where the file lies; an import marked
366
389
  // `webpackIgnore`/`@vite-ignore` is meant to stay a runtime import, as Next's bundle keeps it.
367
- const IMPORT_META_PATH = /\bimport\.meta\.(?:url|dirname|filename|resolve)\b/
368
- const COMPUTED_IMPORT =
369
- /(?<![.\w$])import\s*\(((?:\s|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*)(?!['"`)\s/])/g
370
-
371
- function readsBesideItself(source: string) {
372
- if (IMPORT_META_PATH.test(source)) return true
373
- for (const match of source.matchAll(COMPUTED_IMPORT)) {
374
- if (!/webpackIgnore:\s*true|@vite-ignore/.test(match[1]!)) return true
375
- }
376
- return false
390
+ /** Reads its own location or imports a computed path (unless it opts out with a magic comment). */
391
+ function readsBesideItself(file: string, source: string) {
392
+ const refs = importMetaRefs(file, source)
393
+ if (refs.readsLocation) return true
394
+ return refs.computedImports.some(
395
+ call => !/webpackIgnore:\s*true|@vite-ignore/.test(source.slice(call.start, call.end)),
396
+ )
377
397
  }
378
398
 
379
399
  /** Package dirs whose files read beside themselves or import computed paths. */
@@ -392,7 +412,7 @@ async function inPlacePackages(
392
412
  await Promise.all(
393
413
  installed.map(async file => {
394
414
  const source = await readFile(file, 'utf8').catch(() => '')
395
- if (readsBesideItself(source)) files.add(file)
415
+ if (readsBesideItself(file, source)) files.add(file)
396
416
  }),
397
417
  )
398
418
  return [
@@ -411,9 +431,9 @@ async function inPlacePackages(
411
431
  */
412
432
  function sharedCommonJsBody(file: string, code: string, texts: ReadonlyMap<string, string>) {
413
433
  const wrapper = /(?:^|\n)export default (require_[\w$]+)\(\);?/.exec(code)?.[1]
434
+ // The chunk that defines the wrapper, from the parsed imports: `$` in its name is no regex hazard.
414
435
  const chunk =
415
- wrapper &&
416
- new RegExp(`import\\s*\\{[^}]*\\b${wrapper}\\b[^}]*\\}\\s*from\\s*"([^"]+)"`).exec(code)?.[1]
436
+ wrapper && scanFacts(file, code).imports.find(edge => edge.exports.includes(wrapper))?.specifier
417
437
  const text = chunk && texts.get(path.resolve(path.dirname(file), chunk))
418
438
  if (!text) return code
419
439
  const start = text.indexOf(`var ${wrapper} = `)
@@ -742,7 +762,12 @@ const vendorPortablePath = (config: ResolvedConfig, file: string) =>
742
762
  devSourceIdentity(file, config.workspaceRoot)
743
763
 
744
764
  function vendorArtifactFile(config: ResolvedConfig, key: string) {
745
- return path.join(config.outPath, 'cache', 'server', 'vendor', `${hashBundleSpecifier(key)}.mjs`)
765
+ return path.join(
766
+ outCachePath(config.outPath),
767
+ 'server',
768
+ 'vendor',
769
+ `${hashBundleSpecifier(key)}.mjs`,
770
+ )
746
771
  }
747
772
 
748
773
  /**
@@ -1041,7 +1066,7 @@ function reusableVendorContent(
1041
1066
  loadVendorSharedEntries(config)
1042
1067
  const id = vendorSharedFor(config).get(entry)
1043
1068
  if (!id) return undefined
1044
- const content = vendorContentFile(path.join(config.outPath, 'cache', 'server', 'vendor'), id)
1069
+ const content = vendorContentFile(path.join(outCachePath(config.outPath), 'server', 'vendor'), id)
1045
1070
  return existsSync(content) ? content : undefined
1046
1071
  }
1047
1072
 
@@ -1704,9 +1729,6 @@ function packageExportSubpaths(packageRoot: string) {
1704
1729
  }
1705
1730
 
1706
1731
  /** A top-level binding created by `new` or a non-PURE call: an identity each copy would duplicate. */
1707
- const MODULE_STATE =
1708
- /^(?:export\s+)?(?:const|let|var)\s[^=;]*=\s*(?:(?:\/\*\s*[#@]__PURE__\s*\*\/\s*)?new\s|(?!require\s*\()[\w$.]+\s*\()/m
1709
-
1710
1732
  async function statefulPackageFiles(entry: string, packageRoot: string) {
1711
1733
  const files = [...(await packageReach(entry, packageRoot))]
1712
1734
  const sources = await Promise.all(files.map(file => packageFileScan(file)))
@@ -1728,7 +1750,7 @@ function packageFileScan(file: string) {
1728
1750
  .scanImports(source)
1729
1751
  .map(({ path: specifier }) => specifier)
1730
1752
  .filter(specifier => specifier.startsWith('.')),
1731
- stateful: MODULE_STATE.test(source),
1753
+ stateful: hasModuleState(file, source),
1732
1754
  }))
1733
1755
  .catch(() => undefined)
1734
1756
  packageFileScans.set(file, scan)
package/src/types.ts CHANGED
@@ -45,9 +45,8 @@ export interface PNextConfig {
45
45
  }
46
46
 
47
47
  /**
48
- * What a deployment adapter packs into the server function. It ships the closure the runtime can reach, minus
49
- * build debris and files no runtime reads - serverless size limits are hard. These lists are the escape hatch
50
- * when that classification is wrong for an app: `exclude` drops more, `keep` overrides a built-in drop.
48
+ * What `<outDir>/standalone` ships beyond the files its server imports. `exclude` drops traced
49
+ * `node_modules` files; `keep` ships app files the server reads at runtime, at their root-relative paths.
51
50
  * Entries are directory names (`storybook-static`) or file suffixes (`.map`).
52
51
  */
53
52
  export interface PNextAdapterConfig {