@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.
- package/dist/server/_chunks/{aliases-j47a10ce.js → aliases-pycbtjt1.js} +7 -5
- package/dist/server/_chunks/{analyze-arswqe6s.js → analyze-4d9e00g9.js} +17 -13
- package/dist/server/_chunks/{boot-af0mv3dy.js → boot-bhemypxz.js} +13 -11
- package/dist/server/_chunks/{build-spy3dxp4.js → build-c991dv65.js} +7 -5
- package/dist/server/_chunks/{build-t9h0hec3.js → build-kh5e3x48.js} +977 -1186
- package/dist/server/_chunks/{build-tier-m7yvqz61.js → build-tier-0a96kmx3.js} +59 -75
- package/dist/server/_chunks/{compile-53j02j27.js → compile-040r79gj.js} +2 -2
- package/dist/server/_chunks/{config-n2z7yxps.js → config-0tvz03a6.js} +6 -4
- package/dist/server/_chunks/{config-1yzwexq6.js → config-27rj7830.js} +12 -3
- package/dist/server/_chunks/{create-meec9hs6.js → create-9rvwshsp.js} +4 -3
- package/dist/server/_chunks/{dev-fjmqg6en.js → dev-ex9exynt.js} +2 -2
- package/dist/server/_chunks/{generate-n9sh6hzt.js → generate-myw9hef7.js} +5 -4
- package/dist/server/_chunks/{index-91x12ykq.js → index-0r5eqfc7.js} +6 -1
- package/dist/server/_chunks/{index-3xbr4eyv.js → index-0yjv5v47.js} +3 -3
- package/dist/server/_chunks/{index-ncnes8c6.js → index-11xpbfej.js} +2 -2
- package/dist/server/_chunks/{index-357na4a8.js → index-141vcwk6.js} +1 -1
- package/dist/server/_chunks/{index-nyrba2kr.js → index-1pzm1hdy.js} +11 -11
- package/dist/server/_chunks/{index-fkp000p1.js → index-29pq0kax.js} +2 -2
- package/dist/server/_chunks/{index-wpfhfbs4.js → index-38xpdxwd.js} +4 -4
- package/dist/server/_chunks/{index-tvqkq6e6.js → index-3e075kny.js} +1 -1
- package/dist/server/_chunks/{index-kq8kpzye.js → index-3ycf5ckv.js} +3 -3
- package/dist/server/_chunks/{index-20ppawv7.js → index-4da76t62.js} +3 -3
- package/dist/server/_chunks/{index-cf4rvxv6.js → index-52k54ykj.js} +1 -1
- package/dist/server/_chunks/{index-x93hv0sc.js → index-71885zaq.js} +23 -10
- package/dist/server/_chunks/{index-jepjvcrf.js → index-8ct7rqy3.js} +1 -1
- package/dist/server/_chunks/{index-sjpckxqn.js → index-9f49w76j.js} +1 -1
- package/dist/server/_chunks/{index-4hgjrqh7.js → index-9hz3s0m8.js} +2 -2
- package/dist/server/_chunks/{index-9n2914ww.js → index-akkrvjn6.js} +1 -1
- package/dist/server/_chunks/{index-9ych9ma8.js → index-b7j671dj.js} +2 -2
- package/dist/server/_chunks/index-bzd2ef7t.js +10 -0
- package/dist/server/_chunks/{index-p77b4kcx.js → index-cn86z041.js} +1 -1
- package/dist/server/_chunks/{index-hkdfj58q.js → index-cvr4z22m.js} +1 -1
- package/dist/server/_chunks/{index-y2jr2n72.js → index-d158y67j.js} +4 -4
- package/dist/server/_chunks/{index-yz54pxrh.js → index-d1jsmhhk.js} +4 -9
- package/dist/server/_chunks/{index-ggk2j965.js → index-dgr390q7.js} +56 -50
- package/dist/server/_chunks/{index-sctcgyvv.js → index-drwv61cc.js} +2 -2
- package/dist/server/_chunks/{index-9hx2641q.js → index-entm8bmz.js} +1 -1
- package/dist/server/_chunks/{index-jx7bqvw1.js → index-evhd413j.js} +1 -1
- package/dist/server/_chunks/{index-1011cscr.js → index-fjv96re1.js} +61 -29
- package/dist/server/_chunks/{index-c0g2wf3z.js → index-gcmj16xy.js} +1 -1
- package/dist/server/_chunks/{index-ytjvpbm2.js → index-gkndxkxf.js} +3 -3
- package/dist/server/_chunks/index-hdbwtdv2.js +352 -0
- package/dist/server/_chunks/{index-z84ab1w0.js → index-he6n2bvp.js} +3 -3
- package/dist/server/_chunks/{index-eajaspjk.js → index-j0m1jxqa.js} +1 -1
- package/dist/server/_chunks/{index-4q2gbfwk.js → index-j681c48a.js} +2 -2
- package/dist/server/_chunks/index-jbj7ehef.js +50 -0
- package/dist/server/_chunks/{index-8k7pfejn.js → index-jn9rbhrx.js} +7 -7
- package/dist/server/_chunks/index-jt8e1pje.js +12 -0
- package/dist/server/_chunks/{index-eeaanp2e.js → index-kdhxpwyq.js} +3 -3
- package/dist/server/_chunks/{index-6e4fqv30.js → index-m63czgq5.js} +2 -0
- package/dist/server/_chunks/{index-2dv4y4wd.js → index-n629dy38.js} +16 -10
- package/dist/server/_chunks/{index-sv8m6789.js → index-nf3df4jy.js} +5 -5
- package/dist/server/_chunks/{index-06nyqntx.js → index-q3r9z8mq.js} +1 -1
- package/dist/server/_chunks/{index-3b3at80h.js → index-qcrvt6er.js} +6 -3
- package/dist/server/_chunks/{index-egykcqg3.js → index-rgj5mx84.js} +1 -1
- package/dist/server/_chunks/{index-vvmmknm1.js → index-rjegyfx4.js} +19 -15
- package/dist/server/_chunks/{index-ym67zqxp.js → index-tf4j19ge.js} +1 -1
- package/dist/server/_chunks/{index-5gyr4emz.js → index-tq3b39a5.js} +4 -4
- package/dist/server/_chunks/{index-qn4z5s3c.js → index-typcbcsv.js} +3 -3
- package/dist/server/_chunks/{index-11sfk4hb.js → index-vmr35jjw.js} +1 -1
- package/dist/server/_chunks/{index-danncfhy.js → index-wm36rj4v.js} +1 -1
- package/dist/server/_chunks/{index-3tf1xjsp.js → index-x2m17v9c.js} +11 -11
- package/dist/server/_chunks/{index-z4z9zv3b.js → index-x9c55pzy.js} +141 -3
- package/dist/server/_chunks/{index-k1g3epw0.js → index-xh1nbk46.js} +10 -10
- package/dist/server/_chunks/{index-0374j2b9.js → index-y4d02znn.js} +1 -1
- package/dist/server/_chunks/{index-qm6kmhqs.js → index-y7mzvq2q.js} +1 -1
- package/dist/server/_chunks/{index-rn8dyr1f.js → index-yrjkqtfj.js} +8 -48
- package/dist/server/_chunks/{index-tq0pc7eg.js → index-zfrwq472.js} +3 -3
- package/dist/server/_chunks/{loader-7yw0j98z.js → loader-5en7wpt5.js} +12 -10
- package/dist/server/_chunks/{modules-jn8bmbn8.js → modules-t92favpj.js} +14 -12
- package/dist/server/_chunks/{pipeline-yy9jhydf.js → pipeline-4t9q43ht.js} +25 -21
- package/dist/server/_chunks/{production-jar9ztm3.js → production-385bw8df.js} +14 -3
- package/dist/server/_chunks/{proxy-config-j84dgksn.js → proxy-config-60gprphe.js} +7 -5
- package/dist/server/_chunks/{proxy-w1d0wmsn.js → proxy-gfhmktbk.js} +12 -10
- package/dist/server/_chunks/{router-k1reahfn.js → router-xnrva921.js} +10 -7
- package/dist/server/_chunks/{routes-2s277fc6.js → routes-ht868pdc.js} +12 -10
- package/dist/server/_chunks/{run-pvd8bs1t.js → run-d40d63ag.js} +5 -4
- package/dist/server/_chunks/{server-mt4a994c.js → server-2p8fgqq2.js} +38 -34
- package/dist/server/_chunks/{tsconfig-defaults-1nb3308g.js → tsconfig-defaults-bcn34eqb.js} +5 -4
- package/dist/server/_chunks/{typegen-0qr0459b.js → typegen-dvtcjtaw.js} +15 -13
- package/dist/server/cli/index.js +12 -15
- package/dist/server/cli/start.js +35 -14
- package/dist/server/compat/next/cache.js +5 -5
- package/dist/server/compat/next/config-headers.js +8 -6
- package/dist/server/compat/next/config-loader.js +1 -1
- package/dist/server/compat/next/dist/server/web/spec-extension/revalidate.js +5 -5
- package/dist/server/compat/next/dist/server/web/spec-extension/unstable-cache.js +5 -5
- package/dist/server/compat/next/dist/server/web/spec-extension/unstable-no-store.js +5 -5
- package/dist/server/compat/next/dynamic.js +3 -14
- package/dist/server/compat/next/font/cache.js +6 -4
- package/dist/server/compat/next/font/local.js +7 -5
- package/dist/server/compat/next/font/runtime.js +7 -5
- package/dist/server/compat/next/headers.js +4 -4
- package/dist/server/compat/next/image/config.js +2 -2
- package/dist/server/compat/next/image/optimizer.js +3 -3
- package/dist/server/compat/next/image/static-metadata.js +4 -4
- package/dist/server/compat/next/legacy-request-apis.js +3 -3
- package/dist/server/compat/next/link-transform.js +4 -3
- package/dist/server/compat/next/navigation.js +5 -4
- package/dist/server/compat/next/redirects.js +8 -6
- package/dist/server/compat/next/resource-hints.js +2 -2
- package/dist/server/compat/next/rewrites.js +7 -5
- package/dist/server/compat/next/root-params.js +3 -3
- package/dist/server/compat/next/route-state.js +2 -2
- package/dist/server/compat/next/script.js +2 -2
- package/dist/server/compat/next/server.js +5 -5
- package/dist/server/generation.json +1 -1
- package/package.json +1 -1
- package/reference/compat.md +1 -1
- package/reference/config.md +2 -2
- package/reference/dev.md +1 -1
- package/reference/getting-started.md +1 -1
- package/reference/rendering.md +4 -4
- package/src/api/dynamic.tsx +84 -4
- package/src/cli/adapters/vercel-warm.ts +40 -29
- package/src/cli/adapters/vercel.ts +95 -1262
- package/src/cli/analyze.ts +3 -2
- package/src/cli/build/cache.ts +6 -6
- package/src/cli/build.ts +33 -33
- package/src/cli/index.ts +4 -6
- package/src/cli/serve/pipeline.ts +16 -10
- package/src/cli/serve/standalone-worker.ts +8 -0
- package/src/cli/serve/standalone.ts +703 -0
- package/src/cli/start.ts +29 -8
- package/src/client/build.ts +1 -1
- package/src/client/reference-stub.ts +2 -34
- package/src/compat/actions/extensions.ts +3 -2
- package/src/compat/bundler/require-context.ts +2 -1
- package/src/compat/bundler/webpack-loaders.ts +3 -2
- package/src/compat/bundler/worker.ts +2 -2
- package/src/compat/css/inline-css.ts +1 -1
- package/src/compat/export/build-complete.ts +1 -1
- package/src/compat/export/emit.ts +4 -4
- package/src/compat/export/standalone.ts +30 -65
- package/src/compat/image-optimizer/optimize.ts +1 -1
- package/src/compat/lifecycle/instrumentation-client.ts +4 -3
- package/src/compat/metadata-artifacts.ts +1 -1
- package/src/compat/next/config-loader.ts +3 -0
- package/src/compat/next/dynamic.tsx +10 -23
- package/src/compat/next/font/cache.ts +2 -1
- package/src/compat/next/font/runtime.ts +1 -1
- package/src/compat/register/image.ts +1 -1
- package/src/compat/typecheck/check.ts +6 -4
- package/src/config.ts +28 -5
- package/src/css/assets.ts +1 -1
- package/src/css/build.ts +16 -9
- package/src/dev/client-chunk-store.ts +2 -1
- package/src/dev/restart/client-key.ts +2 -1
- package/src/dev/restart/global-css.ts +2 -1
- package/src/dev/restart/route-bundle-key.ts +2 -1
- package/src/dev/restart/route-facts.ts +2 -1
- package/src/dev/server.ts +8 -7
- package/src/out-paths.ts +10 -0
- package/src/render/renderer.ts +1 -1
- package/src/resolve/dynamic.ts +4 -0
- package/src/resolve/engine.ts +2 -12
- package/src/resolve/scan-facts.ts +185 -1
- package/src/routing/metadata-files.ts +1 -1
- package/src/runtime/loader.ts +5 -3
- package/src/runtime/module-cache.ts +2 -1
- package/src/runtime/modules.ts +50 -16
- package/src/runtime/prebundle.ts +23 -3
- package/src/runtime/production.ts +105 -40
- package/src/runtime/release-graph.ts +21 -13
- package/src/runtime/vendor-build.ts +45 -23
- package/src/types.ts +2 -3
- package/src/utils/directive.ts +40 -0
- package/src/utils/fs.ts +7 -2
- package/src/utils/resource-query.ts +12 -0
- package/dist/server/_chunks/index-tqn2qnnn.js +0 -241
- package/dist/server/_chunks/index-y36a0ybw.js +0 -57
- 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
|
+
}
|