@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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/bun-native-source.json +151 -0
  4. package/package.json +63 -4
  5. package/src/.oxfmtrc.json +20 -0
  6. package/src/.oxlintrc.json +12 -0
  7. package/src/experiments/aot-gpui/compile.ts +44 -0
  8. package/src/experiments/aot-gpui/inspect-runtime.ts +304 -0
  9. package/src/experiments/aot-gpui/inspect.ts +66 -0
  10. package/src/scripts/native/artifact.ts +73 -0
  11. package/src/scripts/native/build.ts +231 -0
  12. package/src/scripts/native/bundle.ts +121 -0
  13. package/src/scripts/native/consumer-files.ts +46 -0
  14. package/src/scripts/native/context.ts +95 -0
  15. package/src/scripts/native/doctor.ts +69 -0
  16. package/src/scripts/native/host-download.ts +179 -0
  17. package/src/scripts/native/host-header.ts +33 -0
  18. package/src/scripts/native/host.ts +22 -0
  19. package/src/scripts/native/inputs.ts +95 -0
  20. package/src/scripts/native/lint.ts +43 -0
  21. package/src/scripts/native/package-archive.ts +94 -0
  22. package/src/scripts/native/package-lock.ts +220 -0
  23. package/src/scripts/native/package-set.ts +417 -0
  24. package/src/scripts/native/package-stamp.ts +209 -0
  25. package/src/scripts/native/paths.ts +20 -0
  26. package/src/scripts/native/project.ts +271 -0
  27. package/src/scripts/native/quality.ts +76 -0
  28. package/src/scripts/native/registry-project.ts +90 -0
  29. package/src/scripts/native/session.ts +299 -0
  30. package/src/scripts/native/standalone.ts +64 -0
  31. package/src/scripts/native/ustar.ts +29 -0
  32. package/src/scripts/native/watch.ts +104 -0
  33. package/src/scripts/native.ts +237 -0
  34. package/src/scripts/solid/transform.ts +16 -0
  35. package/src/toolchain.json +74 -0
  36. 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
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
+ }