@bun-native/cli 0.0.0-stage → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +4 -2
- package/bun-native-source.json +151 -0
- package/package.json +63 -4
- package/src/.oxfmtrc.json +20 -0
- package/src/.oxlintrc.json +12 -0
- package/src/experiments/aot-gpui/compile.ts +44 -0
- package/src/experiments/aot-gpui/inspect-runtime.ts +304 -0
- package/src/experiments/aot-gpui/inspect.ts +66 -0
- package/src/scripts/native/artifact.ts +73 -0
- package/src/scripts/native/build.ts +231 -0
- package/src/scripts/native/bundle.ts +121 -0
- package/src/scripts/native/consumer-files.ts +46 -0
- package/src/scripts/native/context.ts +95 -0
- package/src/scripts/native/doctor.ts +69 -0
- package/src/scripts/native/host-download.ts +179 -0
- package/src/scripts/native/host-header.ts +33 -0
- package/src/scripts/native/host.ts +22 -0
- package/src/scripts/native/inputs.ts +95 -0
- package/src/scripts/native/lint.ts +43 -0
- package/src/scripts/native/package-archive.ts +94 -0
- package/src/scripts/native/package-lock.ts +220 -0
- package/src/scripts/native/package-set.ts +417 -0
- package/src/scripts/native/package-stamp.ts +209 -0
- package/src/scripts/native/paths.ts +20 -0
- package/src/scripts/native/project.ts +271 -0
- package/src/scripts/native/quality.ts +76 -0
- package/src/scripts/native/registry-project.ts +90 -0
- package/src/scripts/native/session.ts +299 -0
- package/src/scripts/native/standalone.ts +64 -0
- package/src/scripts/native/ustar.ts +29 -0
- package/src/scripts/native/watch.ts +104 -0
- package/src/scripts/native.ts +237 -0
- package/src/scripts/solid/transform.ts +16 -0
- package/src/toolchain.json +74 -0
- package/src/tsconfig.json +45 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
// Layout is pinned to Bun da867c60: src/standalone_graph/StandaloneModuleGraph.rs
|
|
4
|
+
// Offsets is repr(C), 32 bytes on all six supported 64-bit targets. A .bun_aot
|
|
5
|
+
// section also exists in JIT executables; the actual module graph flag matters.
|
|
6
|
+
const trailer = Buffer.from('\n---- Bun! ----\n')
|
|
7
|
+
const offsetsSize = 32
|
|
8
|
+
const moduleRecordSize = 52
|
|
9
|
+
const compiledProgramFlag = 1 << 14
|
|
10
|
+
|
|
11
|
+
/** Inspect an executable by path, or bytes a caller already read. */
|
|
12
|
+
export function inspectBunExecutable(source: string | Buffer): {
|
|
13
|
+
format: 'elf' | 'macho' | 'pe'
|
|
14
|
+
payloadOffset: number
|
|
15
|
+
payloadBytes: number
|
|
16
|
+
flags: number
|
|
17
|
+
hasCompiledProgram: boolean
|
|
18
|
+
} {
|
|
19
|
+
const bytes = typeof source === 'string' ? readFileSync(source) : source
|
|
20
|
+
const magic = bytes.length >= 4 ? bytes.readUInt32LE(0) : 0
|
|
21
|
+
const format =
|
|
22
|
+
magic === 0x464c457f
|
|
23
|
+
? 'elf'
|
|
24
|
+
: magic === 0xfeedfacf
|
|
25
|
+
? 'macho'
|
|
26
|
+
: bytes.subarray(0, 2).toString() === 'MZ'
|
|
27
|
+
? 'pe'
|
|
28
|
+
: undefined
|
|
29
|
+
if (!format) throw new Error('Unsupported executable format')
|
|
30
|
+
|
|
31
|
+
let trailerOffset = bytes.lastIndexOf(trailer)
|
|
32
|
+
while (trailerOffset >= offsetsSize + 8) {
|
|
33
|
+
const offsets = trailerOffset - offsetsSize
|
|
34
|
+
const count64 = bytes.readBigUInt64LE(offsets)
|
|
35
|
+
if (count64 <= BigInt(offsets - 8)) {
|
|
36
|
+
const count = Number(count64)
|
|
37
|
+
const payloadOffset = offsets - count
|
|
38
|
+
const payloadBytes = count + offsetsSize + trailer.length
|
|
39
|
+
const modulesOffset = bytes.readUInt32LE(offsets + 8)
|
|
40
|
+
const modulesLength = bytes.readUInt32LE(offsets + 12)
|
|
41
|
+
const entryPoint = bytes.readUInt32LE(offsets + 16)
|
|
42
|
+
const argvOffset = bytes.readUInt32LE(offsets + 20)
|
|
43
|
+
const argvLength = bytes.readUInt32LE(offsets + 24)
|
|
44
|
+
const flags = bytes.readUInt32LE(offsets + 28)
|
|
45
|
+
if (
|
|
46
|
+
bytes.readBigUInt64LE(payloadOffset - 8) === BigInt(payloadBytes) &&
|
|
47
|
+
modulesLength > 0 &&
|
|
48
|
+
modulesLength % moduleRecordSize === 0 &&
|
|
49
|
+
modulesOffset + modulesLength <= count &&
|
|
50
|
+
entryPoint < modulesLength / moduleRecordSize &&
|
|
51
|
+
argvOffset + argvLength <= count &&
|
|
52
|
+
(flags & ~0x7fff) === 0
|
|
53
|
+
) {
|
|
54
|
+
return {
|
|
55
|
+
format,
|
|
56
|
+
payloadOffset,
|
|
57
|
+
payloadBytes,
|
|
58
|
+
flags,
|
|
59
|
+
hasCompiledProgram: (flags & compiledProgramFlag) !== 0,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
trailerOffset = bytes.lastIndexOf(trailer, trailerOffset - 1)
|
|
64
|
+
}
|
|
65
|
+
throw new Error('No valid pinned Bun standalone module graph found')
|
|
66
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export type FileDigest = { bytes: number; sha256: string }
|
|
5
|
+
|
|
6
|
+
// Source files and manifests are read whole; compilers and host binaries are
|
|
7
|
+
// hashed through one reused buffer so memory stays bounded.
|
|
8
|
+
const WHOLE_FILE_BYTES = 4 * 1024 * 1024
|
|
9
|
+
const CHUNK_BYTES = 1024 * 1024
|
|
10
|
+
|
|
11
|
+
/** SHA-256 hex through Bun's native hasher; avoids loading node:crypto on CLI paths. */
|
|
12
|
+
export function sha256(data: string | Uint8Array): string {
|
|
13
|
+
return new Bun.CryptoHasher('sha256').update(data).digest('hex')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function fileDigest(path: string): Promise<FileDigest> {
|
|
17
|
+
const hash = new Bun.CryptoHasher('sha256')
|
|
18
|
+
// The size only picks a strategy; both read to EOF and count what they read.
|
|
19
|
+
if ((await stat(path)).size <= WHOLE_FILE_BYTES) {
|
|
20
|
+
const data = await readFile(path)
|
|
21
|
+
return { bytes: data.length, sha256: hash.update(data).digest('hex') }
|
|
22
|
+
}
|
|
23
|
+
let bytes = 0
|
|
24
|
+
const file = await open(path, 'r')
|
|
25
|
+
try {
|
|
26
|
+
const buffer = Buffer.allocUnsafe(CHUNK_BYTES)
|
|
27
|
+
for (;;) {
|
|
28
|
+
const { bytesRead } = await file.read(buffer, 0, CHUNK_BYTES, null)
|
|
29
|
+
if (bytesRead === 0) break
|
|
30
|
+
hash.update(buffer.subarray(0, bytesRead))
|
|
31
|
+
bytes += bytesRead
|
|
32
|
+
}
|
|
33
|
+
} finally {
|
|
34
|
+
await file.close()
|
|
35
|
+
}
|
|
36
|
+
return { bytes, sha256: hash.digest('hex') }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The digest of a large, rarely changing file (the compiler), reused from `cache`
|
|
41
|
+
* while the file keeps its device, inode, size, mtime and ctime. Any write or
|
|
42
|
+
* replacement changes one of those, and the file is hashed again.
|
|
43
|
+
*/
|
|
44
|
+
export async function cachedFileDigest(path: string, cache: string): Promise<FileDigest> {
|
|
45
|
+
const identity = async () => {
|
|
46
|
+
const stats = await stat(path, { bigint: true })
|
|
47
|
+
return [stats.dev, stats.ino, stats.size, stats.mtimeNs, stats.ctimeNs].join(':')
|
|
48
|
+
}
|
|
49
|
+
const before = await identity()
|
|
50
|
+
try {
|
|
51
|
+
const cached = await Bun.file(cache).json()
|
|
52
|
+
if (
|
|
53
|
+
cached?.path === path &&
|
|
54
|
+
cached.identity === before &&
|
|
55
|
+
Number.isSafeInteger(cached.digest?.bytes) &&
|
|
56
|
+
/^[0-9a-f]{64}$/.test(cached.digest?.sha256)
|
|
57
|
+
)
|
|
58
|
+
return { bytes: cached.digest.bytes, sha256: cached.digest.sha256 }
|
|
59
|
+
} catch {}
|
|
60
|
+
const digest = await fileDigest(path)
|
|
61
|
+
if ((await identity()) === before) {
|
|
62
|
+
const partial = `${cache}.${process.pid}.partial`
|
|
63
|
+
try {
|
|
64
|
+
await mkdir(dirname(cache), { recursive: true })
|
|
65
|
+
await writeFile(partial, JSON.stringify({ path, identity: before, digest }) + '\n')
|
|
66
|
+
await rename(partial, cache)
|
|
67
|
+
} catch {
|
|
68
|
+
// The cache is optional; a read-only project hashes every time.
|
|
69
|
+
await rm(partial, { force: true }).catch(() => {})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return digest
|
|
73
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { constants } from 'node:fs'
|
|
2
|
+
import { chmod, copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
4
|
+
import { compileApplication } from '../../experiments/aot-gpui/compile.ts'
|
|
5
|
+
import type { Project } from './project.ts'
|
|
6
|
+
import { bundleApplication } from './bundle.ts'
|
|
7
|
+
import { cachedFileDigest, fileDigest, sha256 } from './artifact.ts'
|
|
8
|
+
import { userCache } from './context.ts'
|
|
9
|
+
import toolchain from '../../toolchain.json'
|
|
10
|
+
|
|
11
|
+
/** Bundle a project's entry into `outdir/application.js` with the shared configuration. */
|
|
12
|
+
export async function bundleProject(project: Project, outdir: string): Promise<string> {
|
|
13
|
+
const { module } = await bundleApplication({
|
|
14
|
+
framework: project.framework,
|
|
15
|
+
entry: project.entry,
|
|
16
|
+
outdir,
|
|
17
|
+
cwd: project.directory,
|
|
18
|
+
})
|
|
19
|
+
return module
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The application bundle that holds the host in a macOS build. */
|
|
23
|
+
export const MACOS_HOST_BUNDLE = 'BunNativeHost.app'
|
|
24
|
+
const BUNDLE_IDENTIFIER = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/
|
|
25
|
+
|
|
26
|
+
const xml = (text: string) =>
|
|
27
|
+
text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
28
|
+
|
|
29
|
+
/** The Info.plist that makes the macOS host an application bundle with an identifier. */
|
|
30
|
+
export function hostInfoPlist(identifier: string, name: string, executable: string): string {
|
|
31
|
+
if (!BUNDLE_IDENTIFIER.test(identifier))
|
|
32
|
+
throw new Error(`Bundle identifier ${JSON.stringify(identifier)} is not reverse-DNS text`)
|
|
33
|
+
const entry = (key: string, value: string) =>
|
|
34
|
+
` <key>${key}</key>\n <string>${xml(value)}</string>`
|
|
35
|
+
return [
|
|
36
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
37
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
38
|
+
'<plist version="1.0">',
|
|
39
|
+
'<dict>',
|
|
40
|
+
entry('CFBundleIdentifier', identifier),
|
|
41
|
+
entry('CFBundleExecutable', executable),
|
|
42
|
+
entry('CFBundleName', name),
|
|
43
|
+
entry('CFBundleDisplayName', name),
|
|
44
|
+
entry('CFBundlePackageType', 'APPL'),
|
|
45
|
+
entry('CFBundleInfoDictionaryVersion', '6.0'),
|
|
46
|
+
entry('CFBundleShortVersionString', '1.0'),
|
|
47
|
+
entry('CFBundleVersion', '1'),
|
|
48
|
+
' <key>NSHighResolutionCapable</key>\n <true/>',
|
|
49
|
+
'</dict>',
|
|
50
|
+
'</plist>',
|
|
51
|
+
'',
|
|
52
|
+
].join('\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Where the host goes in a build. macOS user notifications serve only a process
|
|
57
|
+
* that runs from an application bundle with an identifier
|
|
58
|
+
* (docs/decisions/0009-native-notifications.md), so a macOS build puts the host in
|
|
59
|
+
* `BunNativeHost.app/Contents/MacOS` next to its Info.plist; every other platform
|
|
60
|
+
* keeps it adjacent to the application.
|
|
61
|
+
*/
|
|
62
|
+
export function hostLayout(
|
|
63
|
+
platform: NodeJS.Platform,
|
|
64
|
+
hostFile: string
|
|
65
|
+
): { file: string; infoPlist?: string } {
|
|
66
|
+
if (platform !== 'darwin') return { file: hostFile }
|
|
67
|
+
return {
|
|
68
|
+
file: `${MACOS_HOST_BUNDLE}/Contents/MacOS/${hostFile}`,
|
|
69
|
+
infoPlist: `${MACOS_HOST_BUNDLE}/Contents/Info.plist`,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The default bundle identity: the project's package name under `dev.bun-native.app`. */
|
|
74
|
+
export async function defaultBundleIdentity(
|
|
75
|
+
project: Project
|
|
76
|
+
): Promise<{ identifier: string; name: string }> {
|
|
77
|
+
let name = 'application'
|
|
78
|
+
try {
|
|
79
|
+
const value = JSON.parse(await readFile(join(project.directory, 'package.json'), 'utf8')) as {
|
|
80
|
+
name?: unknown
|
|
81
|
+
}
|
|
82
|
+
if (typeof value.name === 'string' && value.name) name = value.name
|
|
83
|
+
} catch {}
|
|
84
|
+
const slug =
|
|
85
|
+
name
|
|
86
|
+
.replace(/^@/, '')
|
|
87
|
+
.replace(/[^A-Za-z0-9-]+/g, '-')
|
|
88
|
+
.replace(/^-+|-+$/g, '') || 'application'
|
|
89
|
+
return { identifier: `dev.bun-native.app.${slug}`, name }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function buildProject(
|
|
93
|
+
project: Project,
|
|
94
|
+
host: string,
|
|
95
|
+
destination: string,
|
|
96
|
+
hostProfile: 'dev' | 'release',
|
|
97
|
+
options: { bundleIdentifier?: string } = {}
|
|
98
|
+
) {
|
|
99
|
+
if (Bun.revision !== toolchain.bun.revision) throw new Error('Use the pinned Bun AOT compiler')
|
|
100
|
+
const outdir = resolve(destination)
|
|
101
|
+
await mkdir(outdir, { recursive: true })
|
|
102
|
+
if ((await readdir(outdir)).length !== 0) throw new Error('Output directory must be empty')
|
|
103
|
+
const suffix = process.platform === 'win32' ? '.exe' : ''
|
|
104
|
+
const layout = hostLayout(process.platform, `bun-native-host${suffix}`)
|
|
105
|
+
const hostFile = layout.file
|
|
106
|
+
const identity = await defaultBundleIdentity(project)
|
|
107
|
+
if (options.bundleIdentifier !== undefined) identity.identifier = options.bundleIdentifier
|
|
108
|
+
// Validate before any output exists.
|
|
109
|
+
const infoPlist = layout.infoPlist
|
|
110
|
+
? hostInfoPlist(identity.identifier, identity.name, `bun-native-host${suffix}`)
|
|
111
|
+
: undefined
|
|
112
|
+
const applicationFile = `application${suffix}`
|
|
113
|
+
const originalHost = await fileDigest(host)
|
|
114
|
+
const module = await bundleProject(project, join(outdir, 'development'))
|
|
115
|
+
const developmentModule = await fileDigest(module)
|
|
116
|
+
// Resolve the bundled host relative to the executable, independent of launch cwd.
|
|
117
|
+
const launcher = join(outdir, 'launcher.ts')
|
|
118
|
+
await writeFile(
|
|
119
|
+
launcher,
|
|
120
|
+
`import { dirname, join } from 'node:path'\nprocess.env['BUN_NATIVE_HOST'] ??= join(dirname(process.execPath), ${JSON.stringify(hostFile)})\nawait import('./development/application.js')\n`,
|
|
121
|
+
{ flag: 'wx' }
|
|
122
|
+
)
|
|
123
|
+
// The compiler names the program by its path relative to its working directory,
|
|
124
|
+
// so compiling the relative launcher from the output directory keeps the
|
|
125
|
+
// executable independent of where that directory is.
|
|
126
|
+
const compiled = compileApplication('launcher.ts', join(outdir, applicationFile), outdir)
|
|
127
|
+
if (layout.infoPlist && infoPlist) {
|
|
128
|
+
await mkdir(dirname(join(outdir, hostFile)), { recursive: true })
|
|
129
|
+
await writeFile(join(outdir, layout.infoPlist), infoPlist, { flag: 'wx' })
|
|
130
|
+
}
|
|
131
|
+
await copyFile(host, join(outdir, hostFile), constants.COPYFILE_EXCL)
|
|
132
|
+
if (process.platform !== 'win32') await chmod(join(outdir, hostFile), 0o755)
|
|
133
|
+
const packagedHost = await fileDigest(join(outdir, hostFile))
|
|
134
|
+
if (
|
|
135
|
+
originalHost.sha256 !== packagedHost.sha256 ||
|
|
136
|
+
originalHost.sha256 !== (await fileDigest(host)).sha256
|
|
137
|
+
)
|
|
138
|
+
throw new Error('Prebuilt host changed while packaging; retry with a stable input')
|
|
139
|
+
if (developmentModule.sha256 !== (await fileDigest(module)).sha256)
|
|
140
|
+
throw new Error('Application module changed during AOT compilation')
|
|
141
|
+
// Keep machine-local paths and wall-clock timing out of the manifest so equal
|
|
142
|
+
// inputs produce an identical manifest; timing goes to a separate diagnostic.
|
|
143
|
+
const { compileCommand, buildMs, executable, ...inspected } = compiled
|
|
144
|
+
await writeFile(
|
|
145
|
+
join(outdir, 'build-metrics.json'),
|
|
146
|
+
JSON.stringify({ schemaVersion: 1, compileMs: buildMs }, null, 2) + '\n',
|
|
147
|
+
{ flag: 'wx' }
|
|
148
|
+
)
|
|
149
|
+
const manifest = {
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
...(project.packageSet ? { packageSet: project.packageSet } : {}),
|
|
152
|
+
framework: project.framework,
|
|
153
|
+
entrypoint: relative(project.directory, project.entry),
|
|
154
|
+
target: { os: process.platform, architecture: process.arch },
|
|
155
|
+
toolchain,
|
|
156
|
+
application: {
|
|
157
|
+
file: applicationFile,
|
|
158
|
+
...executable,
|
|
159
|
+
// Absolute arguments are recorded relative to this artifact directory; the
|
|
160
|
+
// compiler is identified by its digest instead of its install path.
|
|
161
|
+
compileCommand: compileCommand.map((argument, index) =>
|
|
162
|
+
index === 0 ? 'bun' : isAbsolute(argument) ? relative(outdir, argument) : argument
|
|
163
|
+
),
|
|
164
|
+
compiler: {
|
|
165
|
+
revision: Bun.revision,
|
|
166
|
+
executable: await cachedFileDigest(
|
|
167
|
+
process.execPath,
|
|
168
|
+
join(userCache(), 'compiler', `${sha256(process.execPath).slice(0, 32)}.json`)
|
|
169
|
+
),
|
|
170
|
+
},
|
|
171
|
+
...inspected,
|
|
172
|
+
},
|
|
173
|
+
developmentModule: { file: 'development/application.js', ...developmentModule },
|
|
174
|
+
sourceMap: {
|
|
175
|
+
file: 'development/application.js.map',
|
|
176
|
+
...(await fileDigest(join(outdir, 'development/application.js.map'))),
|
|
177
|
+
},
|
|
178
|
+
launcher: { file: 'launcher.ts', ...(await fileDigest(launcher)) },
|
|
179
|
+
reproducibility: {
|
|
180
|
+
deterministic: [
|
|
181
|
+
applicationFile,
|
|
182
|
+
hostFile,
|
|
183
|
+
...(layout.infoPlist ? [layout.infoPlist] : []),
|
|
184
|
+
'development/application.js',
|
|
185
|
+
'development/application.js.map',
|
|
186
|
+
'launcher.ts',
|
|
187
|
+
'manifest.json',
|
|
188
|
+
],
|
|
189
|
+
diagnostics: ['build-metrics.json', 'compile-aot.stdout.log', 'compile-aot.stderr.log'],
|
|
190
|
+
},
|
|
191
|
+
host: {
|
|
192
|
+
file: hostFile,
|
|
193
|
+
profile: hostProfile,
|
|
194
|
+
...packagedHost,
|
|
195
|
+
...(layout.infoPlist
|
|
196
|
+
? {
|
|
197
|
+
bundle: {
|
|
198
|
+
directory: MACOS_HOST_BUNDLE,
|
|
199
|
+
identifier: identity.identifier,
|
|
200
|
+
infoPlist: {
|
|
201
|
+
file: layout.infoPlist,
|
|
202
|
+
...(await fileDigest(join(outdir, layout.infoPlist))),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
: {}),
|
|
207
|
+
provenance:
|
|
208
|
+
'Caller-supplied prebuilt executable. Profile is caller-declared; match the hash to native build evidence.',
|
|
209
|
+
},
|
|
210
|
+
launch: {
|
|
211
|
+
executable: applicationFile,
|
|
212
|
+
host:
|
|
213
|
+
process.platform === 'darwin'
|
|
214
|
+
? 'Bundled host inside BunNativeHost.app, so macOS serves it user notifications; BUN_NATIVE_HOST can explicitly override it'
|
|
215
|
+
: 'Adjacent bundled host; BUN_NATIVE_HOST can explicitly override it',
|
|
216
|
+
},
|
|
217
|
+
verification: {
|
|
218
|
+
aotCompiledProgram: 'passed',
|
|
219
|
+
jitOmission: 'passed',
|
|
220
|
+
nativeRendering: 'not-run',
|
|
221
|
+
osInput: 'not-run',
|
|
222
|
+
teardown: 'not-run',
|
|
223
|
+
signing: 'unsigned',
|
|
224
|
+
installation: 'not-tested',
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
await writeFile(join(outdir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', {
|
|
228
|
+
flag: 'wx',
|
|
229
|
+
})
|
|
230
|
+
return manifest
|
|
231
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { BuildConfig, BunPlugin } from 'bun'
|
|
2
|
+
import { mkdir, realpath, stat } from 'node:fs/promises'
|
|
3
|
+
import { join, resolve, sep } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export type Framework = 'solid' | 'react'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The one bundler configuration for a Bun Native application module. `native dev`,
|
|
9
|
+
* `native build`, the CI fixtures and the benchmarks all bundle through it, so CI
|
|
10
|
+
* qualifies the bytes users build. The module is not minified: `native dev` runs it
|
|
11
|
+
* as is (readable stacks), and the AOT compiler's `--production` minifies the
|
|
12
|
+
* compiled program anyway.
|
|
13
|
+
*/
|
|
14
|
+
export function bundleConfig(
|
|
15
|
+
framework: Framework,
|
|
16
|
+
entry: string,
|
|
17
|
+
outdir: string,
|
|
18
|
+
plugins: BunPlugin[] = []
|
|
19
|
+
): BuildConfig & { outdir: string } {
|
|
20
|
+
return {
|
|
21
|
+
entrypoints: [entry],
|
|
22
|
+
outdir,
|
|
23
|
+
naming: 'application.js',
|
|
24
|
+
target: 'bun',
|
|
25
|
+
format: 'esm',
|
|
26
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
27
|
+
sourcemap: 'external',
|
|
28
|
+
metafile: true,
|
|
29
|
+
plugins,
|
|
30
|
+
// Solid's universal renderer is selected through the `browser` export condition;
|
|
31
|
+
// React uses its automatic JSX runtime from the adapter.
|
|
32
|
+
...(framework === 'solid'
|
|
33
|
+
? { conditions: ['browser'] }
|
|
34
|
+
: { jsx: { runtime: 'automatic', importSource: '@bun-native/react' } }),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Solid's universal JSX transform as a bundler plugin; Babel loads only for Solid builds. */
|
|
39
|
+
async function solidPlugin(): Promise<BunPlugin> {
|
|
40
|
+
const { transformSolid } = await import('../solid/transform.ts')
|
|
41
|
+
return {
|
|
42
|
+
name: 'solid-native-universal',
|
|
43
|
+
setup(build) {
|
|
44
|
+
build.onLoad({ filter: /\.[jt]sx$/ }, async ({ path }) => ({
|
|
45
|
+
contents: transformSolid(await Bun.file(path).text(), path),
|
|
46
|
+
loader: 'ts',
|
|
47
|
+
}))
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Bundle = {
|
|
53
|
+
/** The absolute path of `application.js`; its source map is next to it. */
|
|
54
|
+
module: string
|
|
55
|
+
/** Every module the bundle read, relative to `cwd`, `/`-separated and sorted (from the metafile). */
|
|
56
|
+
inputs: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Bundle one application entry into `outdir/application.js`.
|
|
61
|
+
*
|
|
62
|
+
* Bun.build names each module in its `// path` comments relative to the process
|
|
63
|
+
* working directory, so the bundle runs with the working directory set to `cwd` (the
|
|
64
|
+
* project, or the checkout for fixtures) and equal sources give equal bytes from any
|
|
65
|
+
* caller directory.
|
|
66
|
+
*/
|
|
67
|
+
export async function bundleApplication(options: {
|
|
68
|
+
framework: Framework
|
|
69
|
+
entry: string
|
|
70
|
+
outdir: string
|
|
71
|
+
cwd: string
|
|
72
|
+
plugins?: BunPlugin[]
|
|
73
|
+
}): Promise<Bundle> {
|
|
74
|
+
const entry = await realpath(options.entry)
|
|
75
|
+
if (!(await stat(entry)).isFile() || !/\.[jt]sx?$/.test(entry))
|
|
76
|
+
throw new Error(
|
|
77
|
+
`${options.framework === 'solid' ? 'Solid' : 'React'} entry must be a TypeScript/JavaScript source file`
|
|
78
|
+
)
|
|
79
|
+
const outdir = resolve(options.outdir)
|
|
80
|
+
await mkdir(outdir, { recursive: true })
|
|
81
|
+
const plugins =
|
|
82
|
+
options.plugins ?? (options.framework === 'solid' ? [await solidPlugin()] : undefined)
|
|
83
|
+
const previous = process.cwd()
|
|
84
|
+
process.chdir(options.cwd)
|
|
85
|
+
let result: Awaited<ReturnType<typeof Bun.build>>
|
|
86
|
+
try {
|
|
87
|
+
result = await Bun.build(bundleConfig(options.framework, entry, outdir, plugins))
|
|
88
|
+
} finally {
|
|
89
|
+
process.chdir(previous)
|
|
90
|
+
}
|
|
91
|
+
if (!result.success)
|
|
92
|
+
throw new AggregateError(
|
|
93
|
+
result.logs,
|
|
94
|
+
`${options.framework === 'solid' ? 'Solid' : 'React'} native build failed`
|
|
95
|
+
)
|
|
96
|
+
return {
|
|
97
|
+
module: join(outdir, 'application.js'),
|
|
98
|
+
inputs: Object.keys(result.metafile?.inputs ?? {})
|
|
99
|
+
.map((path) => path.split(sep).join('/'))
|
|
100
|
+
.sort(),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The modules an entry that is compiled directly (without a framework bundle) reads,
|
|
106
|
+
* from the same resolution `bun compile` uses; nothing is written.
|
|
107
|
+
*/
|
|
108
|
+
export async function moduleInputs(entry: string, cwd: string): Promise<string[]> {
|
|
109
|
+
const previous = process.cwd()
|
|
110
|
+
process.chdir(cwd)
|
|
111
|
+
let result: Awaited<ReturnType<typeof Bun.build>>
|
|
112
|
+
try {
|
|
113
|
+
result = await Bun.build({ entrypoints: [entry], target: 'bun', format: 'esm', metafile: true })
|
|
114
|
+
} finally {
|
|
115
|
+
process.chdir(previous)
|
|
116
|
+
}
|
|
117
|
+
if (!result.success) throw new AggregateError(result.logs, `Cannot resolve ${entry}`)
|
|
118
|
+
return Object.keys(result.metafile?.inputs ?? {})
|
|
119
|
+
.map((path) => path.split(sep).join('/'))
|
|
120
|
+
.sort()
|
|
121
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import compilerConfig from '../../tsconfig.json'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Files shared by every project that installs Bun Native from packages (a local
|
|
5
|
+
* package set or the npm registry) instead of using a checkout's workspace.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The installed CLI, run without auto-install so a missing package fails instead of downloading. */
|
|
9
|
+
export const installedCli = 'bun --no-install ./node_modules/@bun-native/cli/src/scripts/native.ts'
|
|
10
|
+
|
|
11
|
+
export const json = (value: unknown) => JSON.stringify(value, null, 2) + '\n'
|
|
12
|
+
|
|
13
|
+
/** The template formatter keeps short string arrays on one line, so format checks pass as generated. */
|
|
14
|
+
const formattedJSON = (value: unknown) =>
|
|
15
|
+
json(value).replace(
|
|
16
|
+
/\[\n\s*("[^"\n]*"(?:,\n\s*"[^"\n]*")*)\n\s*\]/g,
|
|
17
|
+
(_, items: string) => `[${items.split(/,\n\s*/).join(', ')}]`
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
export function consumerScripts(): Record<string, string> {
|
|
21
|
+
return {
|
|
22
|
+
// The persistent watch controller returns freed bundler pages promptly;
|
|
23
|
+
// see docs/developer-tooling.md "Controller memory".
|
|
24
|
+
dev: `MIMALLOC_PURGE_DELAY=0 ${installedCli} dev .`,
|
|
25
|
+
build: `${installedCli} build .`,
|
|
26
|
+
check: 'bun --no-install check --no-script --no-pretty',
|
|
27
|
+
format: `${installedCli} format .`,
|
|
28
|
+
'format:check': `${installedCli} format . --check`,
|
|
29
|
+
lint: `${installedCli} lint .`,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function consumerTsconfig(
|
|
34
|
+
framework: 'solid' | 'react',
|
|
35
|
+
entry: string,
|
|
36
|
+
exclude: string[]
|
|
37
|
+
): string {
|
|
38
|
+
return formattedJSON({
|
|
39
|
+
compilerOptions: {
|
|
40
|
+
...compilerConfig.compilerOptions,
|
|
41
|
+
jsxImportSource: `@bun-native/${framework}`,
|
|
42
|
+
},
|
|
43
|
+
include: [entry],
|
|
44
|
+
exclude,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { basename, dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where this CLI runs from, computed once per invocation.
|
|
6
|
+
*
|
|
7
|
+
* - `checkout`: `scripts/native.ts` in a Bun Native checkout. The version is the
|
|
8
|
+
* workspace's root `package.json`; released hosts are cached in the checkout's
|
|
9
|
+
* `.bun-native/hosts`.
|
|
10
|
+
* - `package`: the installed `@bun-native/cli`, whose sources live under `src/`.
|
|
11
|
+
* The version is the package's own manifest. Released hosts are cached in the
|
|
12
|
+
* per-user cache, never inside the digest-verified CLI package and never in the
|
|
13
|
+
* consumer project, whose `.bun-native/` holds only `dev` modules.
|
|
14
|
+
*/
|
|
15
|
+
export type CliContext = {
|
|
16
|
+
mode: 'checkout' | 'package'
|
|
17
|
+
/** The directory that holds `scripts/native.ts`: the checkout root, or the package's `src`. */
|
|
18
|
+
root: string
|
|
19
|
+
/** Absent only for a bare copy of the CLI sources; host downloads then need --version. */
|
|
20
|
+
version: string | undefined
|
|
21
|
+
hostCache: string
|
|
22
|
+
/** How the usage text spells this CLI. */
|
|
23
|
+
command: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Environment = Record<string, string | undefined>
|
|
27
|
+
|
|
28
|
+
const packageCommand = 'bun --no-install ./node_modules/@bun-native/cli/src/scripts/native.ts'
|
|
29
|
+
|
|
30
|
+
async function manifest(path: string): Promise<{ name?: unknown; version?: unknown } | undefined> {
|
|
31
|
+
const file = Bun.file(path)
|
|
32
|
+
return (await file.exists()) ? file.json() : undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The per-user cache for state the CLI keeps between runs (released hosts,
|
|
37
|
+
* verification stamps, the compiler digest). Projects stay free of it.
|
|
38
|
+
*/
|
|
39
|
+
export function userCache(
|
|
40
|
+
environment: Environment = process.env,
|
|
41
|
+
platform: NodeJS.Platform = process.platform,
|
|
42
|
+
home: string = homedir()
|
|
43
|
+
): string {
|
|
44
|
+
const base =
|
|
45
|
+
environment['XDG_CACHE_HOME'] ||
|
|
46
|
+
(platform === 'darwin'
|
|
47
|
+
? join(home, 'Library', 'Caches')
|
|
48
|
+
: platform === 'win32'
|
|
49
|
+
? environment['LOCALAPPDATA'] || join(home, 'AppData', 'Local')
|
|
50
|
+
: join(home, '.cache'))
|
|
51
|
+
return join(base, 'bun-native')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Released hosts of an installed CLI. */
|
|
55
|
+
export function userHostCache(
|
|
56
|
+
environment: Environment = process.env,
|
|
57
|
+
platform: NodeJS.Platform = process.platform,
|
|
58
|
+
home: string = homedir()
|
|
59
|
+
): string {
|
|
60
|
+
return join(userCache(environment, platform, home), 'hosts')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function cliContext(
|
|
64
|
+
root: string,
|
|
65
|
+
environment: Environment = process.env
|
|
66
|
+
): Promise<CliContext> {
|
|
67
|
+
const packageRoot = dirname(root)
|
|
68
|
+
const installed =
|
|
69
|
+
basename(root) === 'src' ? await manifest(join(packageRoot, 'package.json')) : undefined
|
|
70
|
+
if (installed?.name === '@bun-native/cli') {
|
|
71
|
+
return {
|
|
72
|
+
mode: 'package',
|
|
73
|
+
root,
|
|
74
|
+
version: typeof installed.version === 'string' ? installed.version : undefined,
|
|
75
|
+
hostCache: userHostCache(environment),
|
|
76
|
+
command: packageCommand,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const workspace = await manifest(join(root, 'package.json'))
|
|
80
|
+
return {
|
|
81
|
+
mode: 'checkout',
|
|
82
|
+
root,
|
|
83
|
+
version: typeof workspace?.version === 'string' ? workspace.version : undefined,
|
|
84
|
+
hostCache: join(root, '.bun-native', 'hosts'),
|
|
85
|
+
command: 'bun run native',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The release version for host downloads: an explicit one, else this CLI's own. */
|
|
90
|
+
export function releaseVersion(context: CliContext, explicit?: string): string {
|
|
91
|
+
const version = explicit ?? context.version
|
|
92
|
+
if (version === undefined)
|
|
93
|
+
throw new Error('This CLI has no package version; pass host install --version x.y.z')
|
|
94
|
+
return version
|
|
95
|
+
}
|