@wular/pnext 0.1.7 → 0.1.8

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-pycbtjt1.js} +7 -5
  2. package/dist/server/_chunks/{analyze-arswqe6s.js → analyze-4d9e00g9.js} +17 -13
  3. package/dist/server/_chunks/{boot-af0mv3dy.js → boot-bhemypxz.js} +13 -11
  4. package/dist/server/_chunks/{build-spy3dxp4.js → build-c991dv65.js} +7 -5
  5. package/dist/server/_chunks/{build-t9h0hec3.js → build-kh5e3x48.js} +977 -1186
  6. package/dist/server/_chunks/{build-tier-m7yvqz61.js → build-tier-0a96kmx3.js} +59 -75
  7. package/dist/server/_chunks/{compile-53j02j27.js → compile-040r79gj.js} +2 -2
  8. package/dist/server/_chunks/{config-n2z7yxps.js → config-0tvz03a6.js} +6 -4
  9. package/dist/server/_chunks/{config-1yzwexq6.js → config-27rj7830.js} +12 -3
  10. package/dist/server/_chunks/{create-meec9hs6.js → create-9rvwshsp.js} +4 -3
  11. package/dist/server/_chunks/{dev-fjmqg6en.js → dev-ex9exynt.js} +2 -2
  12. package/dist/server/_chunks/{generate-n9sh6hzt.js → generate-myw9hef7.js} +5 -4
  13. package/dist/server/_chunks/{index-91x12ykq.js → index-0r5eqfc7.js} +6 -1
  14. package/dist/server/_chunks/{index-3xbr4eyv.js → index-0yjv5v47.js} +3 -3
  15. package/dist/server/_chunks/{index-ncnes8c6.js → index-11xpbfej.js} +2 -2
  16. package/dist/server/_chunks/{index-357na4a8.js → index-141vcwk6.js} +1 -1
  17. package/dist/server/_chunks/{index-nyrba2kr.js → index-1pzm1hdy.js} +11 -11
  18. package/dist/server/_chunks/{index-fkp000p1.js → index-29pq0kax.js} +2 -2
  19. package/dist/server/_chunks/{index-wpfhfbs4.js → index-38xpdxwd.js} +4 -4
  20. package/dist/server/_chunks/{index-tvqkq6e6.js → index-3e075kny.js} +1 -1
  21. package/dist/server/_chunks/{index-kq8kpzye.js → index-3ycf5ckv.js} +3 -3
  22. package/dist/server/_chunks/{index-20ppawv7.js → index-4da76t62.js} +3 -3
  23. package/dist/server/_chunks/{index-cf4rvxv6.js → index-52k54ykj.js} +1 -1
  24. package/dist/server/_chunks/{index-x93hv0sc.js → index-71885zaq.js} +23 -10
  25. package/dist/server/_chunks/{index-jepjvcrf.js → index-8ct7rqy3.js} +1 -1
  26. package/dist/server/_chunks/{index-sjpckxqn.js → index-9f49w76j.js} +1 -1
  27. package/dist/server/_chunks/{index-4hgjrqh7.js → index-9hz3s0m8.js} +2 -2
  28. package/dist/server/_chunks/{index-9n2914ww.js → index-akkrvjn6.js} +1 -1
  29. package/dist/server/_chunks/{index-9ych9ma8.js → index-b7j671dj.js} +2 -2
  30. package/dist/server/_chunks/index-bzd2ef7t.js +10 -0
  31. package/dist/server/_chunks/{index-p77b4kcx.js → index-cn86z041.js} +1 -1
  32. package/dist/server/_chunks/{index-hkdfj58q.js → index-cvr4z22m.js} +1 -1
  33. package/dist/server/_chunks/{index-y2jr2n72.js → index-d158y67j.js} +4 -4
  34. package/dist/server/_chunks/{index-yz54pxrh.js → index-d1jsmhhk.js} +4 -9
  35. package/dist/server/_chunks/{index-ggk2j965.js → index-dgr390q7.js} +56 -50
  36. package/dist/server/_chunks/{index-sctcgyvv.js → index-drwv61cc.js} +2 -2
  37. package/dist/server/_chunks/{index-9hx2641q.js → index-entm8bmz.js} +1 -1
  38. package/dist/server/_chunks/{index-jx7bqvw1.js → index-evhd413j.js} +1 -1
  39. package/dist/server/_chunks/{index-1011cscr.js → index-fjv96re1.js} +61 -29
  40. package/dist/server/_chunks/{index-c0g2wf3z.js → index-gcmj16xy.js} +1 -1
  41. package/dist/server/_chunks/{index-ytjvpbm2.js → index-gkndxkxf.js} +3 -3
  42. package/dist/server/_chunks/index-hdbwtdv2.js +352 -0
  43. package/dist/server/_chunks/{index-z84ab1w0.js → index-he6n2bvp.js} +3 -3
  44. package/dist/server/_chunks/{index-eajaspjk.js → index-j0m1jxqa.js} +1 -1
  45. package/dist/server/_chunks/{index-4q2gbfwk.js → index-j681c48a.js} +2 -2
  46. package/dist/server/_chunks/index-jbj7ehef.js +50 -0
  47. package/dist/server/_chunks/{index-8k7pfejn.js → index-jn9rbhrx.js} +7 -7
  48. package/dist/server/_chunks/index-jt8e1pje.js +12 -0
  49. package/dist/server/_chunks/{index-eeaanp2e.js → index-kdhxpwyq.js} +3 -3
  50. package/dist/server/_chunks/{index-6e4fqv30.js → index-m63czgq5.js} +2 -0
  51. package/dist/server/_chunks/{index-2dv4y4wd.js → index-n629dy38.js} +16 -10
  52. package/dist/server/_chunks/{index-sv8m6789.js → index-nf3df4jy.js} +5 -5
  53. package/dist/server/_chunks/{index-06nyqntx.js → index-q3r9z8mq.js} +1 -1
  54. package/dist/server/_chunks/{index-3b3at80h.js → index-qcrvt6er.js} +6 -3
  55. package/dist/server/_chunks/{index-egykcqg3.js → index-rgj5mx84.js} +1 -1
  56. package/dist/server/_chunks/{index-vvmmknm1.js → index-rjegyfx4.js} +19 -15
  57. package/dist/server/_chunks/{index-ym67zqxp.js → index-tf4j19ge.js} +1 -1
  58. package/dist/server/_chunks/{index-5gyr4emz.js → index-tq3b39a5.js} +4 -4
  59. package/dist/server/_chunks/{index-qn4z5s3c.js → index-typcbcsv.js} +3 -3
  60. package/dist/server/_chunks/{index-11sfk4hb.js → index-vmr35jjw.js} +1 -1
  61. package/dist/server/_chunks/{index-danncfhy.js → index-wm36rj4v.js} +1 -1
  62. package/dist/server/_chunks/{index-3tf1xjsp.js → index-x2m17v9c.js} +11 -11
  63. package/dist/server/_chunks/{index-z4z9zv3b.js → index-x9c55pzy.js} +141 -3
  64. package/dist/server/_chunks/{index-k1g3epw0.js → index-xh1nbk46.js} +10 -10
  65. package/dist/server/_chunks/{index-0374j2b9.js → index-y4d02znn.js} +1 -1
  66. package/dist/server/_chunks/{index-qm6kmhqs.js → index-y7mzvq2q.js} +1 -1
  67. package/dist/server/_chunks/{index-rn8dyr1f.js → index-yrjkqtfj.js} +8 -48
  68. package/dist/server/_chunks/{index-tq0pc7eg.js → index-zfrwq472.js} +3 -3
  69. package/dist/server/_chunks/{loader-7yw0j98z.js → loader-5en7wpt5.js} +12 -10
  70. package/dist/server/_chunks/{modules-jn8bmbn8.js → modules-t92favpj.js} +14 -12
  71. package/dist/server/_chunks/{pipeline-yy9jhydf.js → pipeline-4t9q43ht.js} +25 -21
  72. package/dist/server/_chunks/{production-jar9ztm3.js → production-385bw8df.js} +14 -3
  73. package/dist/server/_chunks/{proxy-config-j84dgksn.js → proxy-config-60gprphe.js} +7 -5
  74. package/dist/server/_chunks/{proxy-w1d0wmsn.js → proxy-gfhmktbk.js} +12 -10
  75. package/dist/server/_chunks/{router-k1reahfn.js → router-xnrva921.js} +10 -7
  76. package/dist/server/_chunks/{routes-2s277fc6.js → routes-ht868pdc.js} +12 -10
  77. package/dist/server/_chunks/{run-pvd8bs1t.js → run-d40d63ag.js} +5 -4
  78. package/dist/server/_chunks/{server-mt4a994c.js → server-2p8fgqq2.js} +38 -34
  79. package/dist/server/_chunks/{tsconfig-defaults-1nb3308g.js → tsconfig-defaults-bcn34eqb.js} +5 -4
  80. package/dist/server/_chunks/{typegen-0qr0459b.js → typegen-dvtcjtaw.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 +95 -1262
  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 +703 -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
@@ -0,0 +1,703 @@
1
+ /**
2
+ * `<out>/standalone`: the one folder a production server needs. The release's compiled modules move
3
+ * from the build cache into `server/`, every file they import lands in `node_modules/` (APFS clones,
4
+ * never whole package trees), and `server/entry.js` serves it from wherever the folder is copied.
5
+ * Like Next's `.next/standalone`, minus the hand copy of `public/` and `static/`: they are `static/`.
6
+ */
7
+ import { createHash } from 'node:crypto'
8
+ import { constants, existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
9
+ import { copyFile, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
10
+ import { builtinModules } from 'node:module'
11
+ import path from 'node:path'
12
+ import { fileURLToPath, pathToFileURL } from 'node:url'
13
+ import { frameworkRuntimeSpecifiers, type ResolvedConfig } from '../../config'
14
+ import { frameworkFingerprint } from '../../runtime/fingerprint'
15
+ import { cacheRoot, compiledSourceGraph, flushDevModuleCaches } from '../../runtime/module-cache'
16
+ import { PREBUNDLE_STAMP, prebundledFile, readPrebundleStamp } from '../../runtime/prebundle'
17
+ import {
18
+ buildIndexFile,
19
+ type BuildIndex,
20
+ buildManifestFile,
21
+ readBuildIndex,
22
+ releaseSourceFile,
23
+ standaloneOriginsFile,
24
+ } from '../../runtime/production'
25
+ import { toPosixPath } from '../../utils/fs'
26
+ import { splitResourceQuery } from '../../utils/resource-query'
27
+ import { importMetaRefs, moduleSpecifierEdges } from '../../resolve/scan-facts'
28
+ import { spliceSource } from '../../runtime/module-transform'
29
+ import type { BuildManifest } from '../../types'
30
+
31
+ const frameworkRoot = realpathSync(path.resolve(import.meta.dirname, '..', '..', '..'))
32
+ const frameworkPackage = JSON.parse(
33
+ readFileSync(path.join(frameworkRoot, 'package.json'), 'utf8'),
34
+ ) as { name: string; dependencies?: Record<string, string> }
35
+ // The framework loads only what it depends on; feature packages (its optional dependencies, the
36
+ // app's typescript) ship when the app's compiled code imports them.
37
+ const runtimeDependencies = new Set(Object.keys(frameworkPackage.dependencies ?? {}))
38
+ // Compiler-side modules a release never loads, by module (or prebundle chunk) name.
39
+ const buildOnlyModule = /^(?:build|modules|loader|compile|esbuild|dev|run|typegen|analyze|create)$/
40
+ const builtins = new Set(builtinModules)
41
+ const scriptFile = /\.(?:[cm]?[jt]sx?)$/
42
+
43
+ /** The framework module a release serves from: its prebundle when one matches this source. */
44
+ function frameworkStartEntry() {
45
+ return readPrebundleStamp(frameworkRoot, frameworkFingerprint())
46
+ ? path.join(frameworkRoot, 'dist', 'server', 'cli', 'start.js')
47
+ : path.join(frameworkRoot, 'src', 'cli', 'start.ts')
48
+ }
49
+
50
+ /** A trace pass's state, passed from the worker that traces the framework (structured-clone safe). */
51
+ export interface TraceSnapshot {
52
+ files: [string, string][]
53
+ rewritten: [string, string][]
54
+ seen: string[]
55
+ claims: [string, string][]
56
+ standalones: string[]
57
+ }
58
+
59
+ /** The config fields a trace reads; the worker receives only these. */
60
+ type TraceConfig = Pick<ResolvedConfig, 'outPath' | 'root' | 'workspaceRoot'>
61
+
62
+ interface Trace {
63
+ /** Standalone-relative destination -> source file. */
64
+ files: Map<string, string>
65
+ /** Destinations added since the last flush; final once the file that added them is scanned. */
66
+ fresh: string[]
67
+ /** Standalone-relative destination -> rewritten contents. */
68
+ rewritten: Map<string, string>
69
+ }
70
+
71
+ /**
72
+ * Trace the framework's runtime closure now, on a worker thread: it needs no build output, so it runs
73
+ * beside the build without taking its main thread, and `writeStandalone` only adds the app's modules.
74
+ */
75
+ export function startStandalone(config: ResolvedConfig) {
76
+ const input: TraceConfig = {
77
+ outPath: config.outPath,
78
+ root: config.root,
79
+ workspaceRoot: config.workspaceRoot,
80
+ }
81
+ // The closure depends only on the framework and the packages it resolves: reuse it across builds.
82
+ const modules = path.join(config.workspaceRoot, 'node_modules')
83
+ const cacheFile =
84
+ existsSync(modules) &&
85
+ path.join(modules, '.cache', 'pnext', `standalone-${frameworkTraceKey()}.json`)
86
+ if (cacheFile && existsSync(cacheFile)) {
87
+ try {
88
+ return Promise.resolve(JSON.parse(readFileSync(cacheFile, 'utf8')) as TraceSnapshot)
89
+ } catch {
90
+ // A torn or foreign cache file: trace again.
91
+ }
92
+ }
93
+ const framework = new Promise<TraceSnapshot>((resolve, reject) => {
94
+ const worker = new Worker(new URL('./standalone-worker.ts', import.meta.url))
95
+ // Bun's worker: never keeps a failed build's process alive.
96
+ ;(worker as Worker & { unref?: () => void }).unref?.()
97
+ worker.onmessage = event => {
98
+ resolve(event.data as TraceSnapshot)
99
+ worker.terminate()
100
+ }
101
+ worker.onerror = event => {
102
+ reject(new Error(event.message))
103
+ worker.terminate()
104
+ }
105
+ worker.postMessage(input)
106
+ })
107
+ // A worker that cannot start or fails leaves the trace to the main thread.
108
+ .catch(() => traceFramework(input))
109
+ if (cacheFile)
110
+ void framework
111
+ .then(async snapshot => {
112
+ await mkdir(path.dirname(cacheFile), { recursive: true })
113
+ const temporary = `${cacheFile}.${process.pid}.tmp`
114
+ await writeFile(temporary, JSON.stringify(snapshot))
115
+ await rename(temporary, cacheFile)
116
+ })
117
+ .catch(() => undefined)
118
+ return framework
119
+ }
120
+
121
+ /** The framework's source plus every runtime package it resolves, by real path and manifest mtime. */
122
+ function frameworkTraceKey() {
123
+ const startEntry = frameworkStartEntry()
124
+ const packages = [...runtimeDependencies, ...frameworkRuntimeSpecifiers()].map(specifier => {
125
+ const file = resolveEdge(specifier, startEntry)
126
+ return file ? `${file}\0${statSync(file).mtimeMs}` : specifier
127
+ })
128
+ return createHash('sha256')
129
+ .update(JSON.stringify([frameworkFingerprint(), startEntry, packages]))
130
+ .digest('hex')
131
+ .slice(0, 16)
132
+ }
133
+
134
+ /** The framework's runtime closure, from its server entry and runtime specifiers. */
135
+ export async function traceFramework(config: TraceConfig) {
136
+ const tracer = createTracer(config)
137
+ const startEntry = frameworkStartEntry()
138
+ await tracer.add([
139
+ { file: startEntry, app: false },
140
+ ...frameworkRuntimeSpecifiers().flatMap(specifier => {
141
+ const file = resolveEdge(specifier, startEntry)
142
+ return file ? [{ file, app: false }] : []
143
+ }),
144
+ ])
145
+ return tracer.snapshot()
146
+ }
147
+
148
+ /** Assemble `config.outPath` (the standalone dir) once the build index is written. */
149
+ export async function writeStandalone(
150
+ config: ResolvedConfig,
151
+ framework: Promise<TraceSnapshot> = startStandalone(config),
152
+ ) {
153
+ const outPath = config.outPath
154
+ const cache = cacheRoot(outPath)
155
+ const index = readBuildIndex(outPath)
156
+ const manifestFile = buildManifestFile(outPath)
157
+ const manifest = JSON.parse(await readFile(manifestFile, 'utf8')) as BuildManifest
158
+ const startEntry = frameworkStartEntry()
159
+ const snapshot = await framework
160
+ // Copies overlap the trace: the framework's files start now, the app's as each scan completes.
161
+ const copier = createCopier(outPath)
162
+ const rewrittenFramework = new Map(snapshot.rewritten)
163
+ for (const [dest, source] of snapshot.files)
164
+ copier.copy(dest, source, rewrittenFramework.get(dest))
165
+ const tracer = createTracer(config, snapshot, dest =>
166
+ copier.copy(dest, tracer.trace.files.get(dest)!, tracer.trace.rewritten.get(dest)),
167
+ )
168
+ const { trace, place } = tracer
169
+ await tracer.add(
170
+ [
171
+ ...Object.values(index.modules),
172
+ ...(index.dependencies ?? []),
173
+ index.nextConfig,
174
+ index.instrumentation?.file,
175
+ index.instrumentation?.edge,
176
+ ...Object.values(index.compat?.cacheHandlers ?? {}),
177
+ ...Object.values(index.fonts ?? {}).flatMap(font => font.files),
178
+ manifest.proxyModule,
179
+ ...(manifest.actions ?? []).map(action => action.modulePath),
180
+ ].flatMap(artifact => (artifact ? [{ file: path.resolve(outPath, artifact), app: true }] : [])),
181
+ )
182
+
183
+ addAppFiles(config, index, trace)
184
+ tracer.flush()
185
+ await copier.settle()
186
+ // The release names its artifacts out-relative; the compiled ones now live in `server/`.
187
+ const cachePrefix = `${toPosixPath(path.relative(outPath, cache))}/`
188
+ const relocate = (value: unknown): unknown => {
189
+ if (typeof value === 'string')
190
+ return value.startsWith(cachePrefix) ? `server/${value.slice(cachePrefix.length)}` : value
191
+ if (Array.isArray(value)) return value.map(relocate)
192
+ if (value && typeof value === 'object')
193
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, relocate(item)]))
194
+ return value
195
+ }
196
+ const indexFile = buildIndexFile(outPath)
197
+ await writeFile(
198
+ indexFile,
199
+ JSON.stringify(relocate(JSON.parse(await readFile(indexFile, 'utf8')))),
200
+ )
201
+ const shippedManifest = relocate(
202
+ JSON.parse(await readFile(manifestFile, 'utf8')),
203
+ ) as BuildManifest
204
+ // Route source lists outside the app name build-machine paths the server never reads.
205
+ for (const route of shippedManifest.routes) {
206
+ if (route.sourceFiles)
207
+ route.sourceFiles = route.sourceFiles.filter(file => !file.startsWith('../'))
208
+ }
209
+ await writeFile(manifestFile, `${JSON.stringify(shippedManifest, null, 2)}\n`)
210
+ await writeFile(
211
+ path.join(outPath, 'server', 'entry.js'),
212
+ `export * from ${JSON.stringify(relativeSpecifier('server', place(startEntry)))}\n`,
213
+ )
214
+ await writeFile(path.join(outPath, 'package.json'), '{ "type": "module" }\n')
215
+ // Build-machine only: lets a framework copy that is not the shipped one serve this release in place.
216
+ const origins = Object.fromEntries(
217
+ [...trace.files].filter(([dest]) => dest.startsWith('node_modules/')),
218
+ )
219
+ const originsFile = standaloneOriginsFile(outPath)
220
+ await mkdir(path.dirname(originsFile), { recursive: true })
221
+ await writeFile(originsFile, JSON.stringify({ release: outPath, origins }))
222
+ }
223
+
224
+ function setFile(trace: Trace, dest: string, source: string) {
225
+ if (!trace.files.has(dest)) trace.fresh.push(dest)
226
+ trace.files.set(dest, source)
227
+ }
228
+
229
+ /** The standalone trace: files to ship, rewritten sources, and package placement, filled in passes. */
230
+ function createTracer(config: TraceConfig, from?: TraceSnapshot, onReady?: (dest: string) => void) {
231
+ const outPath = config.outPath
232
+ const cache = cacheRoot(outPath)
233
+ const trace: Trace = {
234
+ files: new Map(from?.files),
235
+ fresh: [],
236
+ rewritten: new Map(from?.rewritten),
237
+ }
238
+ const claims = new Map(from?.claims)
239
+ const place = createPlacer(outPath, cache, claims)
240
+ const standalones = new Set(from?.standalones)
241
+ // Framework and installed package files ship; app sources never do.
242
+ const shipped = (file: string) =>
243
+ installed(file) || (isInside(frameworkRoot, file) && !isInside(config.root, file))
244
+ const seen = new Set(from?.seen)
245
+ // Reads start as files are discovered, so I/O overlaps the scan instead of serializing it.
246
+ const reads = new Map<string, Promise<string | undefined>>()
247
+ const prefetch = (file: string) => {
248
+ if (scriptFile.test(file) && !reads.has(file))
249
+ reads.set(
250
+ file,
251
+ readFile(file, 'utf8').catch(() => undefined),
252
+ )
253
+ }
254
+ const queue: { file: string; app: boolean }[] = []
255
+ const enqueue = (file: string, app: boolean) => {
256
+ if (seen.has(`${app}\0${file}`)) return
257
+ queue.push({ file, app })
258
+ prefetch(file)
259
+ }
260
+ // Files whose scan has finished are final: hand them to the copier while the trace goes on.
261
+ const flush = () => {
262
+ for (const dest of trace.fresh.splice(0)) onReady?.(dest)
263
+ }
264
+ const run = async (items: { file: string; app: boolean }[]) => {
265
+ for (const item of items) enqueue(item.file, item.app)
266
+ for (let item = queue.shift(); item; item = queue.shift(), flush()) {
267
+ const { file, app } = item
268
+ if (seen.has(`${app}\0${file}`)) continue
269
+ seen.add(`${app}\0${file}`)
270
+ // Another app's standalone (a dependency serving its own pnext build) ships whole, as built.
271
+ const nested = enclosingStandalone(file, outPath)
272
+ if (nested) {
273
+ if (!standalones.has(nested)) {
274
+ standalones.add(nested)
275
+ addTree(
276
+ nested,
277
+ path.posix.dirname(path.posix.dirname(place(path.join(nested, 'server', 'entry.js')))),
278
+ trace,
279
+ )
280
+ }
281
+ continue
282
+ }
283
+ const dest = place(file)
284
+ setFile(trace, dest, file)
285
+ if (!isInside(outPath, file) && !isInside(cache, file)) addPackageFiles(file, trace, place)
286
+ if (!scriptFile.test(file)) continue
287
+ const release = isInside(outPath, file) || isInside(cache, file)
288
+ const framework =
289
+ !release && isInside(frameworkRoot, file) && !installedIn(frameworkRoot, file)
290
+ prefetch(file)
291
+ const source = await reads.get(file)
292
+ if (source === undefined) continue
293
+ const edits = new Map<string, string>()
294
+ for (const edge of scanEdges(file, source)) {
295
+ if (edge.url) {
296
+ const target = urlTarget(edge.specifier, file)
297
+ if (!target) continue
298
+ const appFile =
299
+ release && !isInside(outPath, target) && !isInside(cache, target) && !shipped(target)
300
+ // An app source named by URL is its identity: it resolves to the app root, never read.
301
+ if (appFile && scriptFile.test(target)) continue
302
+ if (!statSync(target, { throwIfNoEntry: false })?.isFile()) continue
303
+ // A file it reads by URL (a font, data) ships, under the app's own path.
304
+ const at = appFile
305
+ ? toPosixPath(
306
+ path.join('server', 'assets', path.relative(config.workspaceRoot, target)),
307
+ )
308
+ : place(target)
309
+ if (appFile) setFile(trace, at, target)
310
+ else enqueue(target, release)
311
+ if (release) {
312
+ const relative = relativeSpecifier(path.dirname(dest), at)
313
+ if (relative !== edge.specifier) edits.set(edge.specifier, relative)
314
+ }
315
+ continue
316
+ }
317
+ const bare = isPackageSpecifier(edge.specifier)
318
+ const name = bare ? packageName(edge.specifier) : undefined
319
+ if (bare && !name && !edge.specifier.startsWith('#')) continue
320
+ // Its lazy requires are the compiler facades (esbuild, oxc), which only build and dev reach.
321
+ if (name && framework && !app && (edge.require || !runtimeDependencies.has(name))) continue
322
+ const target = resolveEdge(edge.specifier, file, edge.require)
323
+ if (!target) continue
324
+ if (edge.dynamic && framework && buildOnlyModule.test(moduleName(target))) continue
325
+ // An app import reaches one framework module deep: the one it names may load a feature package.
326
+ enqueue(target, release)
327
+ for (const counterpart of frameworkCounterparts(target)) enqueue(counterpart, release)
328
+ // A package placed off its natural `node_modules/<name>` (another version holds it) is named by path.
329
+ if (release || (name && !place(target).startsWith(`node_modules/${name}/`))) {
330
+ const relative = relativeSpecifier(path.dirname(dest), place(target))
331
+ if (relative !== edge.specifier) edits.set(edge.specifier, relative)
332
+ }
333
+ }
334
+ if (edits.size > 0) trace.rewritten.set(dest, rewriteSpecifiers(file, source, edits))
335
+ }
336
+ flush()
337
+ }
338
+ const snapshot = (): TraceSnapshot => ({
339
+ files: [...trace.files],
340
+ rewritten: [...trace.rewritten],
341
+ seen: [...seen],
342
+ claims: [...claims],
343
+ standalones: [...standalones],
344
+ })
345
+ return { trace, place, add: run, snapshot, flush }
346
+ }
347
+
348
+ // Build debris and files nothing reads at request time, pruned at any depth of the app tree.
349
+ const prunedDirNames = [
350
+ 'node_modules',
351
+ '.git',
352
+ '.next',
353
+ '.pnext',
354
+ '.turbo',
355
+ '.cache',
356
+ '.vercel',
357
+ '.tmp',
358
+ ]
359
+ // The standalone dir's own entries, which an app file never overwrites.
360
+ const standaloneEntries = new Set(['server', 'static', 'node_modules', 'package.json'])
361
+
362
+ /**
363
+ * App files ship only when `adapter.keep` names them (a file, a directory, or a `.suffix`), as Next
364
+ * ships only traced files plus `outputFileTracingIncludes`; `.env*` files need their exact name.
365
+ * `adapter.exclude` prunes traced package files.
366
+ */
367
+ function addAppFiles(config: ResolvedConfig, index: BuildIndex, trace: Trace) {
368
+ const { keep = [], exclude = [] } = config.adapter ?? {}
369
+ const named = (entries: string[], name: string) =>
370
+ entries.some(entry => name === entry || (entry.startsWith('.') && name.endsWith(entry)))
371
+ for (const dest of trace.files.keys()) {
372
+ const segments = dest.split('/')
373
+ if (dest.startsWith('node_modules/') && segments.some(name => named(exclude, name)))
374
+ trace.files.delete(dest)
375
+ }
376
+ if (keep.length === 0) return
377
+ const compiled = compiledSources(config, index)
378
+ const outTop = path.relative(config.root, config.outRootPath).split(path.sep)[0]
379
+ const visit = (dir: string, kept: boolean) => {
380
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
381
+ const file = path.join(dir, entry.name)
382
+ if (entry.isDirectory()) {
383
+ const pruned = prunedDirNames.includes(entry.name) && !named(keep, entry.name)
384
+ if (pruned || (dir === config.root && entry.name === outTop)) continue
385
+ visit(file, kept || named(keep, entry.name))
386
+ continue
387
+ }
388
+ if (!entry.isFile() || (compiled.has(file) && !named(keep, entry.name))) continue
389
+ const env = entry.name.startsWith('.env')
390
+ if (env ? !keep.includes(entry.name) : !kept && !named(keep, entry.name)) continue
391
+ const relative = toPosixPath(path.relative(config.root, file))
392
+ if (!standaloneEntries.has(relative.split('/')[0]!)) setFile(trace, relative, file)
393
+ }
394
+ }
395
+ visit(config.root, false)
396
+ }
397
+
398
+ /** App sources the release compiled: its modules' closure over the compile graph, plus the configs. */
399
+ function compiledSources(config: ResolvedConfig, index: BuildIndex) {
400
+ flushDevModuleCaches()
401
+ const graph = compiledSourceGraph(config.outPath, config.workspaceRoot)
402
+ const queue = Object.keys(index.modules).map(key =>
403
+ releaseSourceFile(
404
+ key.slice(key.indexOf(':', key.indexOf(':') + 1) + 1),
405
+ config.root,
406
+ config.workspaceRoot,
407
+ ),
408
+ )
409
+ if (index.proxy) queue.push(path.resolve(config.root, index.proxy.file))
410
+ for (const name of readdirSync(config.root))
411
+ if (
412
+ /^(?:pnext|next|instrumentation)\.config\.|^instrumentation\./.test(name) &&
413
+ scriptFile.test(name)
414
+ )
415
+ queue.push(path.join(config.root, name))
416
+ const compiled = new Set<string>()
417
+ for (const file of queue) {
418
+ if (compiled.has(file)) continue
419
+ compiled.add(file)
420
+ queue.push(...(graph.get(file) ?? []))
421
+ }
422
+ // Global stylesheets compile into `static/` too.
423
+ for (const css of index.globalCss) compiled.add(path.resolve(config.root, css))
424
+ return compiled
425
+ }
426
+
427
+ /**
428
+ * Where files live in the standalone dir, relative to it. Installed packages land flat at
429
+ * `node_modules/<name>` as real files, so the folder survives `npm pack`; a second version of a
430
+ * name nests under `node_modules/.pnext/` and its importers name it by path.
431
+ */
432
+ function createPlacer(outPath: string, cache: string, claims = new Map<string, string>()) {
433
+ const packageDir = (root: string, name: string) => {
434
+ const owner = claims.get(name)
435
+ if (owner === undefined) claims.set(name, root)
436
+ if (owner === undefined || owner === root) return `node_modules/${name}`
437
+ const key = createHash('sha256').update(root).digest('hex').slice(0, 12)
438
+ return `node_modules/.pnext/${key}/node_modules/${name}`
439
+ }
440
+ return (file: string) => {
441
+ if (isInside(cache, file)) return toPosixPath(path.join('server', path.relative(cache, file)))
442
+ if (isInside(outPath, file)) return toPosixPath(path.relative(outPath, file))
443
+ const framework =
444
+ !installed(frameworkRoot) &&
445
+ isInside(frameworkRoot, file) &&
446
+ !installedIn(frameworkRoot, file)
447
+ if (framework) {
448
+ const dir = packageDir(frameworkRoot, frameworkPackage.name)
449
+ return `${dir}/${toPosixPath(path.relative(frameworkRoot, file))}`
450
+ }
451
+ const posix = toPosixPath(file)
452
+ const at = posix.lastIndexOf('/node_modules/')
453
+ if (at !== -1) {
454
+ const rest = posix.slice(at + '/node_modules/'.length).split('/')
455
+ const length = rest[0]!.startsWith('@') ? 2 : 1
456
+ const name = rest.slice(0, length).join('/')
457
+ const root = `${posix.slice(0, at)}/node_modules/${name}`
458
+ return [packageDir(root, name), ...rest.slice(length)].join('/')
459
+ }
460
+ // Workspace source reached through a link: it ships under its package name.
461
+ const root = packageRoot(path.dirname(file))
462
+ const name = root && readPackageName(root)
463
+ if (!root || !name)
464
+ throw new Error(`pnext build: cannot place ${file} in the standalone output`)
465
+ return `${packageDir(root, name)}/${toPosixPath(path.relative(root, file))}`
466
+ }
467
+ }
468
+
469
+ /** The pnext standalone dir enclosing `file`, other than the one being written. */
470
+ const standaloneDirs = new Map<string, string | undefined>()
471
+ function enclosingStandalone(file: string, outPath: string): string | undefined {
472
+ const dir = path.dirname(file)
473
+ if (dir === path.dirname(dir)) return undefined
474
+ if (standaloneDirs.has(dir)) return standaloneDirs.get(dir)
475
+ const own =
476
+ dir !== outPath &&
477
+ existsSync(path.join(dir, 'server', 'build-index.json')) &&
478
+ existsSync(path.join(dir, 'server', 'entry.js'))
479
+ const found = own ? dir : enclosingStandalone(dir, outPath)
480
+ standaloneDirs.set(dir, found)
481
+ return found
482
+ }
483
+
484
+ /** Every file under `root`, symlinks dereferenced, at `dest/<relative>`. */
485
+ function addTree(root: string, dest: string, trace: Trace) {
486
+ const visit = (dir: string, seen: Set<string>) => {
487
+ const real = realpathSync(dir)
488
+ if (seen.has(real)) return
489
+ seen.add(real)
490
+ for (const name of readdirSync(dir)) {
491
+ const file = path.join(dir, name)
492
+ const stat = statSync(file, { throwIfNoEntry: false })
493
+ if (stat?.isDirectory()) visit(file, seen)
494
+ else if (stat?.isFile())
495
+ setFile(trace, `${dest}/${toPosixPath(path.relative(root, file))}`, realpathSync(file))
496
+ }
497
+ }
498
+ visit(root, new Set())
499
+ }
500
+
501
+ function installed(file: string) {
502
+ return toPosixPath(file).includes('/node_modules/')
503
+ }
504
+
505
+ /** Whether `file` lies in a package nested under `root`'s own node_modules. */
506
+ function installedIn(root: string, file: string) {
507
+ return path.relative(root, file).split(path.sep).includes('node_modules')
508
+ }
509
+
510
+ interface Edge {
511
+ specifier: string
512
+ dynamic: boolean
513
+ require?: boolean
514
+ url?: boolean
515
+ }
516
+
517
+ const transpilers = new Map<string, Bun.Transpiler>()
518
+
519
+ function scanEdges(file: string, source: string): Edge[] {
520
+ const ext = path.extname(file)
521
+ const loader = ext === '.tsx' || ext === '.jsx' ? 'tsx' : /\.[cm]?ts$/.test(ext) ? 'ts' : 'js'
522
+ let transpiler = transpilers.get(loader)
523
+ if (!transpiler) {
524
+ transpiler = new Bun.Transpiler({
525
+ loader,
526
+ tsconfig: { compilerOptions: { jsx: 'react-jsx', jsxImportSource: 'preact' } },
527
+ })
528
+ transpilers.set(loader, transpiler)
529
+ }
530
+ let imports: ReturnType<Bun.Transpiler['scanImports']>
531
+ try {
532
+ imports = transpiler.scanImports(source)
533
+ } catch {
534
+ return []
535
+ }
536
+ const edges: Edge[] = imports.map(entry => ({
537
+ specifier: entry.path,
538
+ dynamic: entry.kind === 'dynamic-import',
539
+ require: entry.kind === 'require-call',
540
+ }))
541
+ // A module names files beside itself by `new URL(…, import.meta.url)` and `import.meta.resolve`.
542
+ const refs = importMetaRefs(file, source)
543
+ for (const url of refs.urls) edges.push({ specifier: url.specifier, dynamic: false, url: true })
544
+ for (const ref of refs.resolves) edges.push({ specifier: ref.specifier, dynamic: false })
545
+ if (loader === 'tsx')
546
+ edges.push(
547
+ { specifier: 'preact/jsx-runtime', dynamic: false },
548
+ { specifier: 'preact/jsx-dev-runtime', dynamic: false },
549
+ )
550
+ return edges
551
+ }
552
+
553
+ const resolved = new Map<string, string | undefined>()
554
+
555
+ function resolveEdge(specifier: string, from: string, require = false) {
556
+ if (/^(?:node:|bun:|data:|https?:)/.test(specifier) || specifier === 'bun') return undefined
557
+ // A CommonJS `#` is a filename character; an ESM `?query#hash` takes no part in the lookup.
558
+ const bare = require ? specifier : splitResourceQuery(specifier).path
559
+ if (builtins.has(bare)) return undefined
560
+ const dir = path.dirname(from)
561
+ const key = `${dir}\0${bare}`
562
+ if (resolved.has(key)) return resolved.get(key)
563
+ let target: string | undefined
564
+ try {
565
+ const request = bare.startsWith('file:') ? fileURLToPath(bare) : bare
566
+ target = realpathSync(Bun.resolveSync(request, dir))
567
+ if (!statSync(target).isFile()) target = undefined
568
+ } catch {
569
+ target = undefined
570
+ }
571
+ resolved.set(key, target)
572
+ return target
573
+ }
574
+
575
+ /** The prebundled module a framework source file is served by, when it has one. */
576
+ function frameworkCounterparts(file: string) {
577
+ const counterpart = prebundledFile(file)
578
+ if (counterpart === file) return []
579
+ return [counterpart, path.join(frameworkRoot, PREBUNDLE_STAMP)]
580
+ }
581
+
582
+ /** `_chunks/name-<hash>.js` chunk or `name.ts` module -> `name`. */
583
+ function moduleName(file: string) {
584
+ const base = path.basename(file, path.extname(file))
585
+ const chunk = path.basename(path.dirname(file)) === '_chunks'
586
+ return chunk && isInside(path.join(frameworkRoot, 'dist'), file)
587
+ ? base.replace(/-[a-z0-9]{8}$/, '')
588
+ : base
589
+ }
590
+
591
+ function packageName(specifier: string) {
592
+ if (specifier.startsWith('#')) return undefined
593
+ const parts = specifier.split('/')
594
+ const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
595
+ return name && !builtins.has(name) ? name : undefined
596
+ }
597
+
598
+ /** Every package.json between `file` and its package root: they steer resolution. */
599
+ function addPackageFiles(file: string, trace: Trace, place: (file: string) => string) {
600
+ const inStore = installed(file)
601
+ for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) {
602
+ const manifest = path.join(dir, 'package.json')
603
+ const found = existsSync(manifest)
604
+ if (found) setFile(trace, place(manifest), manifest)
605
+ if (found && dir === frameworkRoot) {
606
+ for (const name of ['tsconfig.json', path.join('config', 'ts', 'react.json')]) {
607
+ const extra = path.join(dir, name)
608
+ if (existsSync(extra)) setFile(trace, place(extra), extra)
609
+ }
610
+ }
611
+ const parent = path.basename(path.dirname(dir))
612
+ const packageTop =
613
+ parent === 'node_modules' ||
614
+ (parent.startsWith('@') && path.basename(path.dirname(path.dirname(dir))) === 'node_modules')
615
+ if (inStore ? packageTop : found && readPackageName(dir)) return
616
+ }
617
+ }
618
+
619
+ function packageRoot(dir: string): string | undefined {
620
+ for (; dir !== path.dirname(dir); dir = path.dirname(dir)) {
621
+ if (existsSync(path.join(dir, 'package.json'))) return dir
622
+ }
623
+ return undefined
624
+ }
625
+
626
+ function readPackageName(dir: string) {
627
+ try {
628
+ return (JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8')) as { name?: string })
629
+ .name
630
+ } catch {
631
+ return undefined
632
+ }
633
+ }
634
+
635
+ function relativeSpecifier(fromDir: string, to: string) {
636
+ const relative = toPosixPath(path.relative(fromDir, to))
637
+ return relative.startsWith('.') ? relative : `./${relative}`
638
+ }
639
+
640
+ /** Swap specifier literals for their standalone locations, by parsed span: never inside a string. */
641
+ function rewriteSpecifiers(file: string, source: string, edits: Map<string, string>) {
642
+ const refs = importMetaRefs(file, source)
643
+ const spans = [...moduleSpecifierEdges(source, file), ...refs.urls, ...refs.resolves]
644
+ const changes = spans.flatMap(span => {
645
+ const value = edits.get(span.specifier)
646
+ return value === undefined
647
+ ? []
648
+ : [{ start: span.start, end: span.end, value: JSON.stringify(value) }]
649
+ })
650
+ changes.sort((a, b) => a.start - b.start)
651
+ return changes.length > 0 ? spliceSource(source, changes) : source
652
+ }
653
+
654
+ /** A `new URL(specifier, import.meta.url)` target: relative to the module, as the URL resolves. */
655
+ function urlTarget(specifier: string, from: string) {
656
+ try {
657
+ const url = new URL(specifier, pathToFileURL(from))
658
+ return url.protocol === 'file:' ? fileURLToPath(url) : undefined
659
+ } catch {
660
+ return undefined
661
+ }
662
+ }
663
+
664
+ /** A bare package specifier, not a path (`.`, `..`, `./x`, `/x`, `file:`). */
665
+ function isPackageSpecifier(specifier: string) {
666
+ if (specifier === '.' || specifier === '..') return false
667
+ if (specifier.startsWith('./') || specifier.startsWith('../')) return false
668
+ return !path.isAbsolute(specifier) && !specifier.startsWith('file:')
669
+ }
670
+
671
+ /** Copies into the standalone dir as files are handed over: clones, or rewritten contents. */
672
+ function createCopier(outPath: string) {
673
+ const dirs = new Map<string, Promise<unknown>>()
674
+ const copied = new Set<string>()
675
+ const pending: Promise<unknown>[] = []
676
+ const ensureDir = (dir: string) => {
677
+ let made = dirs.get(dir)
678
+ if (!made) {
679
+ made = mkdir(dir, { recursive: true })
680
+ dirs.set(dir, made)
681
+ }
682
+ return made
683
+ }
684
+ const copy = (dest: string, source: string, contents?: string) => {
685
+ if (copied.has(dest)) return
686
+ copied.add(dest)
687
+ const target = path.join(outPath, dest)
688
+ pending.push(
689
+ ensureDir(path.dirname(target)).then(async () => {
690
+ if (contents !== undefined) {
691
+ if (target !== source) await rm(target, { force: true })
692
+ return writeFile(target, contents)
693
+ }
694
+ if (target !== source) await copyFile(source, target, constants.COPYFILE_FICLONE)
695
+ }),
696
+ )
697
+ }
698
+ return { copy, settle: () => Promise.all(pending) }
699
+ }
700
+
701
+ function isInside(root: string, file: string) {
702
+ return file === root || file.startsWith(`${root}${path.sep}`)
703
+ }