@bun-native/cli 0.33.0 → 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/README.md CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  Create, check, run and AOT-build Bun Native desktop applications.
4
4
 
5
- Part of [Bun Native](https://github.com/0xPlayerOne/bun-native), an experimental developer preview. Version 0.33.0 was built from [fa8bef754122](https://github.com/0xPlayerOne/bun-native/tree/fa8bef754122621d4235eb06e30bb1085b4f7b1e) and published from GitHub Actions with npm provenance. Use the Bun revision pinned in that commit's toolchain.json. Start with the [getting-started guide](https://github.com/0xPlayerOne/bun-native/blob/fa8bef754122621d4235eb06e30bb1085b4f7b1e/docs/getting-started.md); `bun-native host install` fetches the matching release host. The license notice is in LICENSE.
5
+ Part of [Bun Native](https://github.com/0xPlayerOne/bun-native), an experimental developer preview. Version 0.33.1 was built from [c56fdd675365](https://github.com/0xPlayerOne/bun-native/tree/c56fdd6753653a10144a4167c1d3dfb3c38a0647) and published from GitHub Actions with npm provenance. Use the Bun revision pinned in that commit's toolchain.json. Start with the [getting-started guide](https://github.com/0xPlayerOne/bun-native/blob/c56fdd6753653a10144a4167c1d3dfb3c38a0647/docs/getting-started.md); `bun-native host install` fetches the matching release host. The license notice is in LICENSE.
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "commit": "fa8bef754122621d4235eb06e30bb1085b4f7b1e",
4
- "tree": "f805e2eb19a4e10fc29d1dc827cf5bc617b5773b",
3
+ "commit": "c56fdd6753653a10144a4167c1d3dfb3c38a0647",
4
+ "tree": "c6e81b3f9d041e11dd0413b5fdd7a50910383854",
5
5
  "files": {
6
6
  "package.json": {
7
- "bytes": 2164,
8
- "sha256": "db50b6f81f25c113a46f8c9b038bc548a92effaeb4ad9562e5e95044baa7b66f"
7
+ "bytes": 2115,
8
+ "sha256": "caf866d56f3cd7530d5735ecbf62512e9e22526bc44c75a4b48a18c26352c0b8"
9
9
  },
10
10
  ".oxfmtrc.json": {
11
11
  "bytes": 402,
@@ -16,36 +16,48 @@
16
16
  "sha256": "25e27847d6fd50c27314506d50c83a68764489b84bf91f9c35dceb937134e882"
17
17
  },
18
18
  "experiments/aot-gpui/compile.ts": {
19
- "bytes": 1661,
20
- "sha256": "832cec3a32205995551dd270fdda0fd4bd26f0dfdf5b11d65457c73b88ec2b8f"
19
+ "bytes": 1947,
20
+ "sha256": "3617b537e53c90759591b2240c94f65aae29ca04d5ce3dcb661ec0e26609cc2a"
21
21
  },
22
22
  "experiments/aot-gpui/inspect-runtime.ts": {
23
- "bytes": 12491,
24
- "sha256": "071d377315ba801faad088a2dcf0140598536d38c7112139fd927a649a1ea285"
23
+ "bytes": 12612,
24
+ "sha256": "d1b2bebca588db846fc964177ae8b42fc5ae0474bfa0ebc9ee833a41e240c39d"
25
25
  },
26
26
  "experiments/aot-gpui/inspect.ts": {
27
- "bytes": 2383,
28
- "sha256": "f80f7ec7e8a070f6e9d8478380d193a0120f9b89dedd88422c7f7fd6b3490d48"
27
+ "bytes": 2504,
28
+ "sha256": "e24851ad70d48df0160ed0ace6d39ebcbf7b7845d793cbd3b0b8a42e9736b499"
29
29
  },
30
30
  "scripts/native.ts": {
31
- "bytes": 9688,
32
- "sha256": "2290cf2b6508cc8a08c044ff363edc4a6b9b53b5a69169773d14807c3d4ed6c6"
31
+ "bytes": 10591,
32
+ "sha256": "8e100fe4929660a5415571b8ec7658074cfaa3c60d2c54fbbed6fb3b2a19be42"
33
33
  },
34
34
  "scripts/native/artifact.ts": {
35
- "bytes": 1131,
36
- "sha256": "e0496b30b935f02efb8297b41fd2e7ad310fc58d8a6056c3bd4b0d8c142044c1"
35
+ "bytes": 2745,
36
+ "sha256": "a64e82d74f56b05d01fb9ea954414f39c6a94f4a298515aff9c05b0dd257da4d"
37
37
  },
38
38
  "scripts/native/build.ts": {
39
- "bytes": 10234,
40
- "sha256": "a72c2639c1a5dd4d31eb48cea48c0a010e71b0ea7794ba1eace2d5f35f7677d1"
39
+ "bytes": 9504,
40
+ "sha256": "42cae4ccfb02ade4c170c1a8b1cc2fc364a7bbdf99c2fc39d1118f00e88dcd55"
41
+ },
42
+ "scripts/native/bundle.ts": {
43
+ "bytes": 4215,
44
+ "sha256": "b29afed376f6fd55ac345bf68a16418301191b89d81e9f0cf52cbab0c21e567f"
45
+ },
46
+ "scripts/native/consumer-files.ts": {
47
+ "bytes": 1599,
48
+ "sha256": "5176aeffd47dc99b9cc5d5f6d17dfcfdb982365d2f9fbb7d2260e26fbd5c18a9"
49
+ },
50
+ "scripts/native/context.ts": {
51
+ "bytes": 3416,
52
+ "sha256": "4ca3e7ce77331bec10783c9c9b744f7065363baac7b5888d845062677f5ff524"
41
53
  },
42
54
  "scripts/native/doctor.ts": {
43
- "bytes": 2610,
44
- "sha256": "ca2f57dacf6c929bb336324fc71228c03557b3c38ab9f5f8b77da86cb5048f7c"
55
+ "bytes": 2748,
56
+ "sha256": "425c1d2bb309556ae4b1a7798130361d09e49b6ce2db7493c241e1fb38737f2c"
45
57
  },
46
58
  "scripts/native/host-download.ts": {
47
- "bytes": 6430,
48
- "sha256": "d27ef6968d506ad8adc0b850a1283fe8c41487d9009dd690e75ad5ad379cdc44"
59
+ "bytes": 7676,
60
+ "sha256": "2850dcbd88c377dfb85160af90514fb57ef646f1545dbf89b28d604d87b6041d"
49
61
  },
50
62
  "scripts/native/host-header.ts": {
51
63
  "bytes": 1248,
@@ -56,60 +68,68 @@
56
68
  "sha256": "5e790bbf787bc22e37aecf031520942d96677717b201fd9889ddcb8eaa2715e1"
57
69
  },
58
70
  "scripts/native/inputs.ts": {
59
- "bytes": 2839,
60
- "sha256": "3e98bf1a0208104cc2c18235cfe84fbde719e5e6cf5fc8a4181c3fa69772ce94"
71
+ "bytes": 3783,
72
+ "sha256": "4b817e8c697e64bdf669e072df91dc1ac61400cb45c91fdefd4269f9d1be4b73"
61
73
  },
62
74
  "scripts/native/lint.ts": {
63
- "bytes": 1819,
64
- "sha256": "eaedd87b266d54722c22a4a5b9ddc5606c96cedeb20d21ea9d2381ed5aaadd33"
75
+ "bytes": 1842,
76
+ "sha256": "201e66c00c8c1d861c67a2ed686fb71e9f7830356db83701a1ce851d72f41b95"
65
77
  },
66
78
  "scripts/native/package-archive.ts": {
67
- "bytes": 4945,
68
- "sha256": "249cda6f7edf4eade989c324e473826899bcc0062cdf9cafde76b0d1c074247e"
79
+ "bytes": 4610,
80
+ "sha256": "faea43432b0f85f315f0dc17ad9b0ca3bdabcd5324ce942577a1ac9940a49d65"
69
81
  },
70
82
  "scripts/native/package-lock.ts": {
71
83
  "bytes": 9244,
72
84
  "sha256": "ad8f2b73ca5b88075b503a18c2f0405f13ec60a6c1d9a9d172056d707c0b5e10"
73
85
  },
74
86
  "scripts/native/package-set.ts": {
75
- "bytes": 16263,
76
- "sha256": "b7207b884b90303a0d44dc9a181617c03e24683850fc17a6db802fc740f0a26a"
87
+ "bytes": 16516,
88
+ "sha256": "79fd8a3a0f4e74f4e312cb511beec9e4893eb18b7ddef8c1179dcff47fc3e858"
89
+ },
90
+ "scripts/native/package-stamp.ts": {
91
+ "bytes": 7982,
92
+ "sha256": "b622a5c2e60da5f804425b37181e573008b1c468afbf9cfe6cab472145cc6e3a"
77
93
  },
78
94
  "scripts/native/paths.ts": {
79
95
  "bytes": 806,
80
96
  "sha256": "6ccf669071878d65dbe4686e9b8585bbb217b4a9cd834f7c1866c9448c0c9a55"
81
97
  },
82
98
  "scripts/native/project.ts": {
83
- "bytes": 11175,
84
- "sha256": "c62044f4e0baa004bba1bd27a0ae3a53beb6aa02d558e356464c39fc471b0c61"
99
+ "bytes": 12188,
100
+ "sha256": "80464428b3efd46253e94cb0ab2a7041ba31753fb97f29dde8acba86fa3c0b21"
85
101
  },
86
102
  "scripts/native/quality.ts": {
87
- "bytes": 2380,
88
- "sha256": "5d2c68657e9a85db9e654b1be6ffc437bcd4fdc623854a98eda5f9da842e38cc"
103
+ "bytes": 2917,
104
+ "sha256": "f68f6730c1371cfd63763716256f717b3f52581ee4b07f4bfa2572873ab49ff9"
105
+ },
106
+ "scripts/native/registry-project.ts": {
107
+ "bytes": 4651,
108
+ "sha256": "4a265b611dce5a7d2614abeb04ca010433f75be80837ae997706ae48f03c3814"
89
109
  },
90
110
  "scripts/native/session.ts": {
91
- "bytes": 9635,
92
- "sha256": "def47965ad2d36d95e0f9a5b1f778443e5d811e8a82b9fb094e4ee79916eb26f"
111
+ "bytes": 9768,
112
+ "sha256": "d1132c54f9e11286198be5ac10cd68d388616ea90b2a400181c578faebfc1a2c"
93
113
  },
94
114
  "scripts/native/standalone.ts": {
95
- "bytes": 6195,
96
- "sha256": "c464f48f3c054dbbb057e621b1f4b55e2679b797bca3282561eed39cfe510f6f"
115
+ "bytes": 5235,
116
+ "sha256": "1830b5f41cfa7516560a5b6d1908899dfb2e93425e6c0a09d8cef608d9490517"
117
+ },
118
+ "scripts/native/ustar.ts": {
119
+ "bytes": 1264,
120
+ "sha256": "c8011ecb1de6e4c1065c1e90cede7cc12a1cf7344231267d01472dfacd9d9d88"
97
121
  },
98
122
  "scripts/native/watch.ts": {
99
123
  "bytes": 3793,
100
124
  "sha256": "b5d7f98516135ae1577823ccd6d4647f53d84df7a0f1fa68d4cad5a2c62cdd0f"
101
125
  },
102
- "scripts/solid-build.ts": {
103
- "bytes": 1615,
104
- "sha256": "5f263562cc299642e2971cce1dd913533ff092bc5543abd11d07048e8b3e5908"
105
- },
106
126
  "scripts/solid/transform.ts": {
107
127
  "bytes": 642,
108
128
  "sha256": "281fdbd71b10e613d9080e0494320161e51cfaf9a1c7b9966707eba0dd24ef1c"
109
129
  },
110
130
  "toolchain.json": {
111
- "bytes": 3012,
112
- "sha256": "00c3f707aae8abe9ca75cd2e162f5e1c7c34362a03675da39afead60d856066a"
131
+ "bytes": 3234,
132
+ "sha256": "9f4d9362cb4cc38c0f4e705295789a0cbfec46ffdd007e0541b9441a7c3003a7"
113
133
  },
114
134
  "tsconfig.json": {
115
135
  "bytes": 1104,
@@ -121,8 +141,8 @@
121
141
  }
122
142
  },
123
143
  "toolchain": {
124
- "bytes": 3012,
125
- "sha256": "00c3f707aae8abe9ca75cd2e162f5e1c7c34362a03675da39afead60d856066a"
144
+ "bytes": 3234,
145
+ "sha256": "9f4d9362cb4cc38c0f4e705295789a0cbfec46ffdd007e0541b9441a7c3003a7"
126
146
  },
127
147
  "dependencyLock": {
128
148
  "bytes": 28357,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bun-native/cli",
3
- "version": "0.33.0",
3
+ "version": "0.33.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -31,5 +31,35 @@
31
31
  "publishConfig": {
32
32
  "access": "public",
33
33
  "provenance": true
34
+ },
35
+ "bunNative": {
36
+ "templates": {
37
+ "solid": {
38
+ "dependencies": {
39
+ "@bun-native/core": "0.33.1",
40
+ "@bun-native/solid": "0.33.1",
41
+ "solid-js": "1.9.18"
42
+ },
43
+ "devDependencies": {
44
+ "@bun-native/cli": "0.33.1",
45
+ "@types/bun": "1.4.4",
46
+ "oxlint": "1.92.0"
47
+ }
48
+ },
49
+ "react": {
50
+ "dependencies": {
51
+ "@bun-native/core": "0.33.1",
52
+ "@bun-native/react": "0.33.1",
53
+ "react": "19.3.1"
54
+ },
55
+ "devDependencies": {
56
+ "@bun-native/cli": "0.33.1",
57
+ "@types/bun": "1.4.4",
58
+ "oxlint": "1.92.0",
59
+ "@types/react": "19.3.0",
60
+ "@types/react-reconciler": "0.34.1"
61
+ }
62
+ }
63
+ }
34
64
  }
35
65
  }
@@ -1,4 +1,4 @@
1
- import { writeFileSync } from 'node:fs'
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
  import { inspectBunExecutable } from './inspect'
4
4
  import { inspectBunRuntime } from './inspect-runtime'
@@ -7,14 +7,11 @@ import toolchain from '../../toolchain.json'
7
7
  /** Shared real AOT compiler and executable checks for fixtures and application tooling. */
8
8
  export function compileApplication(input: string, output: string, cwd: string, trim = false) {
9
9
  if (Bun.revision !== toolchain.bun.revision) throw new Error('Use the pinned Bun AOT compiler')
10
+ // toolchain.json is the one record of the compiler arguments (with the pinned revision).
10
11
  const compileCommand = [
11
12
  process.execPath,
12
- '--no-install',
13
- 'compile',
14
- '--no-script',
15
- '--production',
13
+ ...toolchain.bun.compileArguments,
16
14
  ...(trim ? ['--trim'] : []),
17
- '--explain=json',
18
15
  input,
19
16
  '--outfile',
20
17
  output,
@@ -31,11 +28,17 @@ export function compileApplication(input: string, output: string, cwd: string, t
31
28
  if (result.exitCode !== 0)
32
29
  throw new Error(`compile-aot failed (${result.exitCode}): ${result.stderr}`)
33
30
  const buildMs = performance.now() - start
34
- const inspection = inspectBunExecutable(output)
31
+ // Read the executable once for both inspectors and its digest.
32
+ const bytes = readFileSync(output)
33
+ const inspection = inspectBunExecutable(bytes)
35
34
  if (!inspection.hasCompiledProgram)
36
35
  throw new Error('Compiler output has no compiled-program record')
37
- const runtimeFeatures = inspectBunRuntime(output)
36
+ const runtimeFeatures = inspectBunRuntime(bytes)
38
37
  if (runtimeFeatures.jit === 'present' || runtimeFeatures.jitRemovedRanges.count === 0)
39
38
  throw new Error('Compiler output does not record removal of the JIT compiler')
40
- return { compileCommand, buildMs, inspection, runtimeFeatures }
39
+ const executable = {
40
+ bytes: bytes.length,
41
+ sha256: new Bun.CryptoHasher('sha256').update(bytes).digest('hex'),
42
+ }
43
+ return { compileCommand, buildMs, inspection, runtimeFeatures, executable }
41
44
  }
@@ -218,8 +218,9 @@ function overlappingMappings(table: Section[], start: bigint, length: bigint): S
218
218
  )
219
219
  }
220
220
 
221
- export function inspectBunRuntime(path: string) {
222
- const bytes = readFileSync(path)
221
+ /** Inspect an executable by path, or bytes a caller already read. */
222
+ export function inspectBunRuntime(source: string | Buffer) {
223
+ const bytes = typeof source === 'string' ? readFileSync(source) : source
223
224
  const { format, name, table } = imageSections(bytes)
224
225
  const candidates = table.filter((section) => section.name === name)
225
226
  const section = candidates[0]
@@ -8,14 +8,15 @@ const offsetsSize = 32
8
8
  const moduleRecordSize = 52
9
9
  const compiledProgramFlag = 1 << 14
10
10
 
11
- export function inspectBunExecutable(path: string): {
11
+ /** Inspect an executable by path, or bytes a caller already read. */
12
+ export function inspectBunExecutable(source: string | Buffer): {
12
13
  format: 'elf' | 'macho' | 'pe'
13
14
  payloadOffset: number
14
15
  payloadBytes: number
15
16
  flags: number
16
17
  hasCompiledProgram: boolean
17
18
  } {
18
- const bytes = readFileSync(path)
19
+ const bytes = typeof source === 'string' ? readFileSync(source) : source
19
20
  const magic = bytes.length >= 4 ? bytes.readUInt32LE(0) : 0
20
21
  const format =
21
22
  magic === 0x464c457f
@@ -1,5 +1,5 @@
1
- import { createHash } from 'node:crypto'
2
- import { open, readFile, stat } from 'node:fs/promises'
1
+ import { mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
2
+ import { dirname } from 'node:path'
3
3
 
4
4
  export type FileDigest = { bytes: number; sha256: string }
5
5
 
@@ -8,8 +8,13 @@ export type FileDigest = { bytes: number; sha256: string }
8
8
  const WHOLE_FILE_BYTES = 4 * 1024 * 1024
9
9
  const CHUNK_BYTES = 1024 * 1024
10
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
+
11
16
  export async function fileDigest(path: string): Promise<FileDigest> {
12
- const hash = createHash('sha256')
17
+ const hash = new Bun.CryptoHasher('sha256')
13
18
  // The size only picks a strategy; both read to EOF and count what they read.
14
19
  if ((await stat(path)).size <= WHOLE_FILE_BYTES) {
15
20
  const data = await readFile(path)
@@ -30,3 +35,39 @@ export async function fileDigest(path: string): Promise<FileDigest> {
30
35
  }
31
36
  return { bytes, sha256: hash.digest('hex') }
32
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
+ }
@@ -3,48 +3,20 @@ import { chmod, copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/pr
3
3
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
4
4
  import { compileApplication } from '../../experiments/aot-gpui/compile.ts'
5
5
  import type { Project } from './project.ts'
6
- import { fileDigest } from './artifact.ts'
6
+ import { bundleApplication } from './bundle.ts'
7
+ import { cachedFileDigest, fileDigest, sha256 } from './artifact.ts'
8
+ import { userCache } from './context.ts'
7
9
  import toolchain from '../../toolchain.json'
8
10
 
9
- /**
10
- * Bun.build names each module in its `// path` comments relative to the process
11
- * working directory, so bundling from another cwd changed the module bytes. Anchor
12
- * it to the project for the bundle's duration; every path used here is absolute.
13
- */
14
- async function inProjectDirectory<T>(project: Project, bundle: () => Promise<T>): Promise<T> {
15
- const previous = process.cwd()
16
- process.chdir(project.directory)
17
- try {
18
- return await bundle()
19
- } finally {
20
- process.chdir(previous)
21
- }
22
- }
23
-
11
+ /** Bundle a project's entry into `outdir/application.js` with the shared configuration. */
24
12
  export async function bundleProject(project: Project, outdir: string): Promise<string> {
25
- const destination = resolve(outdir)
26
- return inProjectDirectory(project, () => bundleFromProject(project, destination))
27
- }
28
-
29
- async function bundleFromProject(project: Project, outdir: string): Promise<string> {
30
- if (project.framework === 'solid') {
31
- // Only Solid needs the Babel universal transform; React builds skip loading it.
32
- const { buildSolid } = await import('../solid-build.ts')
33
- return buildSolid(project.entry, outdir)
34
- }
35
- await mkdir(outdir, { recursive: true })
36
- const result = await Bun.build({
37
- entrypoints: [project.entry],
13
+ const { module } = await bundleApplication({
14
+ framework: project.framework,
15
+ entry: project.entry,
38
16
  outdir,
39
- naming: 'application.js',
40
- target: 'bun',
41
- format: 'esm',
42
- jsx: { runtime: 'automatic', importSource: '@bun-native/react' },
43
- define: { 'process.env.NODE_ENV': '"production"' },
44
- sourcemap: 'external',
17
+ cwd: project.directory,
45
18
  })
46
- if (!result.success) throw new AggregateError(result.logs, 'React native build failed')
47
- return join(outdir, 'application.js')
19
+ return module
48
20
  }
49
21
 
50
22
  /** The application bundle that holds the host in a macOS build. */
@@ -148,7 +120,10 @@ export async function buildProject(
148
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`,
149
121
  { flag: 'wx' }
150
122
  )
151
- const compiled = compileApplication(launcher, join(outdir, applicationFile), project.directory)
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)
152
127
  if (layout.infoPlist && infoPlist) {
153
128
  await mkdir(dirname(join(outdir, hostFile)), { recursive: true })
154
129
  await writeFile(join(outdir, layout.infoPlist), infoPlist, { flag: 'wx' })
@@ -165,7 +140,7 @@ export async function buildProject(
165
140
  throw new Error('Application module changed during AOT compilation')
166
141
  // Keep machine-local paths and wall-clock timing out of the manifest so equal
167
142
  // inputs produce an identical manifest; timing goes to a separate diagnostic.
168
- const { compileCommand, buildMs, ...inspected } = compiled
143
+ const { compileCommand, buildMs, executable, ...inspected } = compiled
169
144
  await writeFile(
170
145
  join(outdir, 'build-metrics.json'),
171
146
  JSON.stringify({ schemaVersion: 1, compileMs: buildMs }, null, 2) + '\n',
@@ -180,13 +155,19 @@ export async function buildProject(
180
155
  toolchain,
181
156
  application: {
182
157
  file: applicationFile,
183
- ...(await fileDigest(join(outdir, applicationFile))),
158
+ ...executable,
184
159
  // Absolute arguments are recorded relative to this artifact directory; the
185
160
  // compiler is identified by its digest instead of its install path.
186
161
  compileCommand: compileCommand.map((argument, index) =>
187
162
  index === 0 ? 'bun' : isAbsolute(argument) ? relative(outdir, argument) : argument
188
163
  ),
189
- compiler: { revision: Bun.revision, executable: await fileDigest(process.execPath) },
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
+ },
190
171
  ...inspected,
191
172
  },
192
173
  developmentModule: { file: 'development/application.js', ...developmentModule },
@@ -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
+ }