@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 +1 -1
- package/bun-native-source.json +64 -44
- package/package.json +31 -1
- package/src/experiments/aot-gpui/compile.ts +12 -9
- package/src/experiments/aot-gpui/inspect-runtime.ts +3 -2
- package/src/experiments/aot-gpui/inspect.ts +3 -2
- package/src/scripts/native/artifact.ts +44 -3
- package/src/scripts/native/build.ts +22 -41
- 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 +3 -2
- package/src/scripts/native/host-download.ts +64 -24
- package/src/scripts/native/inputs.ts +50 -20
- package/src/scripts/native/lint.ts +2 -2
- package/src/scripts/native/package-archive.ts +7 -16
- package/src/scripts/native/package-set.ts +24 -15
- package/src/scripts/native/package-stamp.ts +209 -0
- package/src/scripts/native/project.ts +44 -24
- package/src/scripts/native/quality.ts +36 -21
- package/src/scripts/native/registry-project.ts +90 -0
- package/src/scripts/native/session.ts +4 -1
- package/src/scripts/native/standalone.ts +8 -27
- package/src/scripts/native/ustar.ts +29 -0
- package/src/scripts/native.ts +62 -44
- package/src/toolchain.json +12 -2
- package/src/scripts/solid-build.ts +0 -43
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { lstat, mkdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
4
|
+
import toolchain from '../../toolchain.json'
|
|
5
|
+
import { sha256, type FileDigest } from './artifact.ts'
|
|
6
|
+
import { userCache } from './context.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A verified standalone package set, as `readProject` reports it.
|
|
10
|
+
*
|
|
11
|
+
* Full verification hashes every archive, gunzips and hashes each member, checks
|
|
12
|
+
* the consumer lock, and walks and hashes every installed file. `build`, `create`
|
|
13
|
+
* and `doctor` always do that. The light commands (`check`, `format`, `lint`,
|
|
14
|
+
* `dev` and each watch remount) may instead accept a stamp that the last full
|
|
15
|
+
* verification left in `.bun-native/verified.json`. The stamp records the
|
|
16
|
+
* manifest digest and the lstat identity (device, inode, mode, size, mtime and
|
|
17
|
+
* ctime in nanoseconds) of every path that verification read: the manifest,
|
|
18
|
+
* each archive, `bun.lock`, `node_modules`, each required package root, each of
|
|
19
|
+
* its directories and installed files, and the verifier's own modules. Any
|
|
20
|
+
* write, replacement, added or removed entry changes one of those identities,
|
|
21
|
+
* and a miss falls back to full verification. A stamp hit trusts file system
|
|
22
|
+
* metadata; it is a cache for local edit loops, not a security boundary.
|
|
23
|
+
*/
|
|
24
|
+
export type VerifiedPackageSet = {
|
|
25
|
+
source: { commit: string; tree: string }
|
|
26
|
+
manifest: FileDigest
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Stamp = {
|
|
30
|
+
schemaVersion: 1
|
|
31
|
+
revision: string
|
|
32
|
+
framework: 'solid' | 'react'
|
|
33
|
+
manifestPath: string
|
|
34
|
+
archiveDirectory: string
|
|
35
|
+
manifest: FileDigest
|
|
36
|
+
source: VerifiedPackageSet['source']
|
|
37
|
+
entries: Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One stamp per project directory, in the user cache so the project stays clean. */
|
|
41
|
+
export const stampFile = (
|
|
42
|
+
directory: string,
|
|
43
|
+
environment: Record<string, string | undefined> = process.env
|
|
44
|
+
) => join(userCache(environment), 'verified', `${sha256(resolve(directory)).slice(0, 32)}.json`)
|
|
45
|
+
|
|
46
|
+
// The modules whose rules decide verification; changing them invalidates every stamp.
|
|
47
|
+
const verifier = [
|
|
48
|
+
'artifact.ts',
|
|
49
|
+
'package-archive.ts',
|
|
50
|
+
'package-lock.ts',
|
|
51
|
+
'package-set.ts',
|
|
52
|
+
'package-stamp.ts',
|
|
53
|
+
'paths.ts',
|
|
54
|
+
].map((name) => join(import.meta.dir, name))
|
|
55
|
+
|
|
56
|
+
/** One path's identity, or undefined for a symlink or other entry the stamp will not trust. */
|
|
57
|
+
async function identity(path: string): Promise<string | undefined> {
|
|
58
|
+
const stats = await lstat(path, { bigint: true })
|
|
59
|
+
if (!stats.isFile() && !stats.isDirectory()) return undefined
|
|
60
|
+
return [stats.dev, stats.ino, stats.mode, stats.size, stats.mtimeNs, stats.ctimeNs].join(':')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function identities(
|
|
64
|
+
directory: string,
|
|
65
|
+
paths: string[]
|
|
66
|
+
): Promise<Record<string, string> | undefined> {
|
|
67
|
+
const values = await Promise.all(
|
|
68
|
+
paths.map((path) => identity(resolve(directory, path)).catch(() => undefined))
|
|
69
|
+
)
|
|
70
|
+
if (values.some((value) => value === undefined)) return undefined
|
|
71
|
+
return Object.fromEntries(paths.map((path, index) => [path, values[index]!]))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const portable = (path: string) => path.split(sep).join('/')
|
|
75
|
+
|
|
76
|
+
async function manifestDigest(path: string): Promise<{ text: string; digest: FileDigest }> {
|
|
77
|
+
const bytes = await Bun.file(path).bytes()
|
|
78
|
+
return {
|
|
79
|
+
text: new TextDecoder().decode(bytes),
|
|
80
|
+
digest: { bytes: bytes.byteLength, sha256: sha256(bytes) },
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The stamped result when every recorded identity still matches, else undefined. */
|
|
85
|
+
async function stamped(
|
|
86
|
+
directory: string,
|
|
87
|
+
manifestPath: string,
|
|
88
|
+
framework: 'solid' | 'react',
|
|
89
|
+
archiveDirectory: string
|
|
90
|
+
): Promise<VerifiedPackageSet | undefined> {
|
|
91
|
+
if (Bun.revision !== toolchain.bun.revision) return undefined
|
|
92
|
+
let stamp: Stamp
|
|
93
|
+
try {
|
|
94
|
+
stamp = await Bun.file(stampFile(directory)).json()
|
|
95
|
+
} catch {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
stamp?.schemaVersion !== 1 ||
|
|
100
|
+
stamp.revision !== Bun.revision ||
|
|
101
|
+
stamp.framework !== framework ||
|
|
102
|
+
stamp.manifestPath !== manifestPath ||
|
|
103
|
+
stamp.archiveDirectory !== archiveDirectory ||
|
|
104
|
+
typeof stamp.entries !== 'object' ||
|
|
105
|
+
stamp.entries === null
|
|
106
|
+
)
|
|
107
|
+
return undefined
|
|
108
|
+
const [current, manifest] = await Promise.all([
|
|
109
|
+
identities(directory, Object.keys(stamp.entries)),
|
|
110
|
+
manifestDigest(manifestPath),
|
|
111
|
+
])
|
|
112
|
+
if (!current || !isDeepStrictEqual(current, stamp.entries)) return undefined
|
|
113
|
+
if (!isDeepStrictEqual(manifest.digest, stamp.manifest)) return undefined
|
|
114
|
+
return { source: stamp.source, manifest: manifest.digest }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Every path full verification reads, relative to the project or absolute for the verifier. */
|
|
118
|
+
async function verifiedPaths(
|
|
119
|
+
directory: string,
|
|
120
|
+
manifestPath: string,
|
|
121
|
+
manifestText: string,
|
|
122
|
+
framework: 'solid' | 'react'
|
|
123
|
+
): Promise<string[]> {
|
|
124
|
+
const { parsePackageSet, requiredPackages } = await import('./package-set.ts')
|
|
125
|
+
const set = parsePackageSet(JSON.parse(manifestText))
|
|
126
|
+
const setDirectory = portable(relative(directory, dirname(manifestPath)))
|
|
127
|
+
const paths = new Set<string>([
|
|
128
|
+
portable(relative(directory, manifestPath)),
|
|
129
|
+
'bun.lock',
|
|
130
|
+
'node_modules',
|
|
131
|
+
...verifier,
|
|
132
|
+
])
|
|
133
|
+
for (const pkg of Object.values(set.packages)) paths.add(`${setDirectory}/${pkg.archive.file}`)
|
|
134
|
+
for (const name of requiredPackages(set, framework)) {
|
|
135
|
+
const root = `node_modules/${name}`
|
|
136
|
+
const parts = root.split('/')
|
|
137
|
+
for (let index = 2; index <= parts.length; index += 1)
|
|
138
|
+
paths.add(parts.slice(0, index).join('/'))
|
|
139
|
+
for (const file of [...Object.keys(set.packages[name]!.files), '.bun-tag']) {
|
|
140
|
+
const segments = file.split('/')
|
|
141
|
+
for (let index = 1; index <= segments.length; index += 1)
|
|
142
|
+
paths.add(`${root}/${segments.slice(0, index).join('/')}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return [...paths].sort()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function writeStamp(directory: string, stamp: Stamp): Promise<void> {
|
|
149
|
+
const file = stampFile(directory)
|
|
150
|
+
const partial = `${file}.${process.pid}.partial`
|
|
151
|
+
try {
|
|
152
|
+
await mkdir(dirname(file), { recursive: true })
|
|
153
|
+
await writeFile(partial, JSON.stringify(stamp, null, 2) + '\n')
|
|
154
|
+
await rename(partial, file)
|
|
155
|
+
} catch {
|
|
156
|
+
// An unwritable user cache still verifies; it only loses the stamp.
|
|
157
|
+
await rm(partial, { force: true }).catch(() => {})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Verify a standalone project's package set and installed packages. With `reuse`,
|
|
163
|
+
* a matching stamp skips the archive and installed-file hashing, and a full
|
|
164
|
+
* verification refreshes the stamp when nothing changed while it ran.
|
|
165
|
+
*/
|
|
166
|
+
export async function verifyStandalone(
|
|
167
|
+
directory: string,
|
|
168
|
+
manifestPath: string,
|
|
169
|
+
framework: 'solid' | 'react',
|
|
170
|
+
reuse: boolean
|
|
171
|
+
): Promise<VerifiedPackageSet> {
|
|
172
|
+
const archiveDirectory = portable(relative(directory, dirname(manifestPath)))
|
|
173
|
+
if (reuse) {
|
|
174
|
+
const hit = await stamped(directory, manifestPath, framework, archiveDirectory)
|
|
175
|
+
if (hit) return hit
|
|
176
|
+
}
|
|
177
|
+
const { readPackageSet, verifyInstalledPackages } = await import('./package-set.ts')
|
|
178
|
+
// Record identities before hashing, so a change during verification cannot be stamped.
|
|
179
|
+
const before = reuse ? await manifestDigest(manifestPath) : undefined
|
|
180
|
+
const paths = before
|
|
181
|
+
? await verifiedPaths(directory, manifestPath, before.text, framework).catch(() => undefined)
|
|
182
|
+
: undefined
|
|
183
|
+
const recorded = paths ? await identities(directory, paths) : undefined
|
|
184
|
+
const set = await readPackageSet(manifestPath)
|
|
185
|
+
await verifyInstalledPackages(directory, set.manifest, framework, archiveDirectory)
|
|
186
|
+
const manifest =
|
|
187
|
+
before?.text === set.text ? before.digest : (await manifestDigest(manifestPath)).digest
|
|
188
|
+
const result = {
|
|
189
|
+
source: { commit: set.manifest.source.commit, tree: set.manifest.source.tree },
|
|
190
|
+
manifest,
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
paths &&
|
|
194
|
+
recorded &&
|
|
195
|
+
before?.text === set.text &&
|
|
196
|
+
isDeepStrictEqual(recorded, await identities(directory, paths))
|
|
197
|
+
)
|
|
198
|
+
await writeStamp(directory, {
|
|
199
|
+
schemaVersion: 1,
|
|
200
|
+
revision: Bun.revision,
|
|
201
|
+
framework,
|
|
202
|
+
manifestPath,
|
|
203
|
+
archiveDirectory,
|
|
204
|
+
manifest,
|
|
205
|
+
source: result.source,
|
|
206
|
+
entries: recorded,
|
|
207
|
+
})
|
|
208
|
+
return result
|
|
209
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { mkdir, realpath, rm, stat, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
3
|
import { inside } from './paths.ts'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import type { FileDigest } from './artifact.ts'
|
|
5
|
+
import { cliContext } from './context.ts'
|
|
6
|
+
import type { RegistryTemplate } from './registry-project.ts'
|
|
7
|
+
// Package-set modules (archive parsing, gzip, lock checks) load only for standalone
|
|
8
|
+
// projects; checkout projects never need them.
|
|
7
9
|
|
|
8
10
|
export type Framework = 'solid' | 'react'
|
|
9
11
|
export type Language = 'typescript' | 'javascript'
|
|
@@ -121,12 +123,18 @@ export async function createProject(
|
|
|
121
123
|
destination: string,
|
|
122
124
|
framework: Framework,
|
|
123
125
|
language: Language,
|
|
124
|
-
packages?: string
|
|
126
|
+
packages?: string,
|
|
127
|
+
registry?: RegistryTemplate
|
|
125
128
|
): Promise<string> {
|
|
126
|
-
const set =
|
|
129
|
+
const set =
|
|
130
|
+
packages === undefined
|
|
131
|
+
? undefined
|
|
132
|
+
: await (await import('./package-set.ts')).readPackageSet(packages)
|
|
133
|
+
// A project with installed packages (a local set or the registry) lives anywhere.
|
|
134
|
+
const installed = set !== undefined || registry !== undefined
|
|
127
135
|
const root = await realpath(repository)
|
|
128
136
|
const directory = resolve(destination)
|
|
129
|
-
if (!
|
|
137
|
+
if (!installed && !inside(root, directory))
|
|
130
138
|
throw new Error('Create the project inside this Bun Native checkout')
|
|
131
139
|
// Resolve existing parents before writing, so a symlink cannot escape the checkout.
|
|
132
140
|
let ancestor = dirname(directory)
|
|
@@ -141,11 +149,11 @@ export async function createProject(
|
|
|
141
149
|
ancestor = parent
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
|
-
if (!
|
|
152
|
+
if (!installed && resolvedAncestor !== root && !inside(root, resolvedAncestor))
|
|
145
153
|
throw new Error('Project parent resolves outside this Bun Native checkout')
|
|
146
154
|
await mkdir(dirname(directory), { recursive: true })
|
|
147
155
|
const parent = await realpath(dirname(directory))
|
|
148
|
-
if (!
|
|
156
|
+
if (!installed && parent !== root && !inside(root, parent))
|
|
149
157
|
throw new Error('Project parent resolves outside this Bun Native checkout')
|
|
150
158
|
await mkdir(directory) // Existing directories, including empty ones, are never overwritten.
|
|
151
159
|
try {
|
|
@@ -187,7 +195,11 @@ export async function createProject(
|
|
|
187
195
|
'README.md': readme(basename(directory), framework, guide),
|
|
188
196
|
[entry]: template(framework, language),
|
|
189
197
|
}
|
|
190
|
-
if (
|
|
198
|
+
if (registry) {
|
|
199
|
+
const { registryFiles } = await import('./registry-project.ts')
|
|
200
|
+
Object.assign(files, registryFiles(registry, framework, entry, basename(directory)))
|
|
201
|
+
} else if (set) {
|
|
202
|
+
const { copyPackageSet, standaloneFiles } = await import('./standalone.ts')
|
|
191
203
|
Object.assign(files, standaloneFiles(set, framework, entry, basename(directory)))
|
|
192
204
|
await copyPackageSet(set, directory)
|
|
193
205
|
}
|
|
@@ -200,7 +212,20 @@ export async function createProject(
|
|
|
200
212
|
}
|
|
201
213
|
}
|
|
202
214
|
|
|
203
|
-
export
|
|
215
|
+
export type ReadProjectOptions = {
|
|
216
|
+
/**
|
|
217
|
+
* `full` (the default) re-verifies a standalone package set completely. `stamp`
|
|
218
|
+
* accepts the last full verification while no verified path changed; see
|
|
219
|
+
* package-stamp.ts. Use it only for commands that never ship an artifact.
|
|
220
|
+
*/
|
|
221
|
+
verification?: 'full' | 'stamp'
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function readProject(
|
|
225
|
+
repository: string,
|
|
226
|
+
path: string,
|
|
227
|
+
options: ReadProjectOptions = {}
|
|
228
|
+
): Promise<Project> {
|
|
204
229
|
const root = await realpath(repository)
|
|
205
230
|
const directory = await realpath(path)
|
|
206
231
|
const value: unknown = await Bun.file(join(directory, 'bun-native.json')).json()
|
|
@@ -229,23 +254,18 @@ export async function readProject(repository: string, path: string): Promise<Pro
|
|
|
229
254
|
const manifestPath = await realpath(join(directory, config['packageSet']))
|
|
230
255
|
if (!inside(directory, manifestPath))
|
|
231
256
|
throw new Error('Project packageSet resolves outside the project')
|
|
232
|
-
const
|
|
233
|
-
await
|
|
257
|
+
const { verifyStandalone } = await import('./package-stamp.ts')
|
|
258
|
+
const packageSet = await verifyStandalone(
|
|
234
259
|
directory,
|
|
235
|
-
|
|
260
|
+
manifestPath,
|
|
236
261
|
config['framework'],
|
|
237
|
-
|
|
262
|
+
options.verification === 'stamp'
|
|
238
263
|
)
|
|
239
|
-
return {
|
|
240
|
-
directory,
|
|
241
|
-
framework: config['framework'],
|
|
242
|
-
entry,
|
|
243
|
-
packageSet: {
|
|
244
|
-
source: { commit: set.manifest.source.commit, tree: set.manifest.source.tree },
|
|
245
|
-
manifest: await fileDigest(manifestPath),
|
|
246
|
-
},
|
|
247
|
-
}
|
|
264
|
+
return { directory, framework: config['framework'], entry, packageSet }
|
|
248
265
|
}
|
|
249
|
-
|
|
266
|
+
// Without a package set, a project outside the checkout installs its packages from
|
|
267
|
+
// the registry; only an installed CLI (not a checkout's) serves such a project.
|
|
268
|
+
if (!inside(root, directory) && (await cliContext(root)).mode !== 'package')
|
|
269
|
+
throw new Error('Project must be inside this Bun Native checkout')
|
|
250
270
|
return { directory, framework: config['framework'], entry }
|
|
251
271
|
}
|
|
@@ -13,49 +13,64 @@ export type QualityStep = {
|
|
|
13
13
|
output: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/** A quality step that runs longer than this is killed and reported as failed. */
|
|
17
|
+
const STEP_TIMEOUT_MS = 10 * 60_000
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs the pinned Bun with `args` in the project directory and captures its combined
|
|
21
|
+
* output. Asynchronous, so `check` runs its steps concurrently; callers print the
|
|
22
|
+
* results in a fixed order.
|
|
23
|
+
*/
|
|
24
|
+
export async function runQualityStep(
|
|
25
|
+
project: Project,
|
|
26
|
+
args: string[]
|
|
27
|
+
): Promise<Omit<QualityStep, 'name'>> {
|
|
18
28
|
const command = [process.execPath, ...args]
|
|
19
|
-
const
|
|
29
|
+
const child = Bun.spawn(command, {
|
|
20
30
|
cwd: project.directory,
|
|
21
31
|
stdout: 'pipe',
|
|
22
32
|
stderr: 'pipe',
|
|
23
33
|
env: { ...process.env, NO_COLOR: '1' },
|
|
34
|
+
timeout: STEP_TIMEOUT_MS,
|
|
24
35
|
})
|
|
36
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
37
|
+
new Response(child.stdout).text(),
|
|
38
|
+
new Response(child.stderr).text(),
|
|
39
|
+
child.exited,
|
|
40
|
+
])
|
|
25
41
|
return {
|
|
26
42
|
command,
|
|
27
|
-
exitCode:
|
|
28
|
-
output: (
|
|
43
|
+
exitCode: child.signalCode ? 1 : exitCode,
|
|
44
|
+
output: (stdout + stderr).trimEnd(),
|
|
29
45
|
}
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
/** Arguments for the built-in formatter, never a same-named package script. */
|
|
33
|
-
export function formatArguments(project: Project, check: boolean): string[] {
|
|
49
|
+
export async function formatArguments(project: Project, check: boolean): Promise<string[]> {
|
|
34
50
|
const entry = relative(project.directory, project.entry).split(sep).join('/')
|
|
35
|
-
const discovered = Bun.
|
|
51
|
+
const discovered = await Bun.spawn(
|
|
36
52
|
[process.execPath, 'fmt', '--no-script', '--find-config-path', entry],
|
|
37
|
-
{ cwd: project.directory, stdout: 'ignore', stderr: 'ignore' }
|
|
38
|
-
)
|
|
53
|
+
{ cwd: project.directory, stdout: 'ignore', stderr: 'ignore', timeout: STEP_TIMEOUT_MS }
|
|
54
|
+
).exited
|
|
39
55
|
const args = ['--no-install', 'fmt', '--no-script', '--no-color']
|
|
40
56
|
if (check) args.push('--check')
|
|
41
|
-
if (discovered
|
|
57
|
+
if (discovered !== 0) args.push('--config', templateFormatterConfig)
|
|
42
58
|
// Package archives and their canonical manifest are verified by digest, never reformatted.
|
|
43
59
|
if (project.packageSet) args.push('--ignore-pattern', 'vendor/**')
|
|
44
60
|
args.push('.')
|
|
45
61
|
return args
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
export function formatProject(project: Project, check: boolean): QualityStep {
|
|
49
|
-
return {
|
|
64
|
+
export async function formatProject(project: Project, check: boolean): Promise<QualityStep> {
|
|
65
|
+
return {
|
|
66
|
+
name: 'format',
|
|
67
|
+
...(await runQualityStep(project, await formatArguments(project, check))),
|
|
68
|
+
}
|
|
50
69
|
}
|
|
51
70
|
|
|
52
|
-
/** Built-in type checking
|
|
53
|
-
export function checkProject(project: Project): QualityStep[] {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
...runQualityStep(project, ['--no-install', 'check', '--no-script', '--no-pretty']),
|
|
58
|
-
},
|
|
59
|
-
formatProject(project, true),
|
|
60
|
-
]
|
|
71
|
+
/** Built-in type checking and the formatter check, run together; one report covers both. */
|
|
72
|
+
export async function checkProject(project: Project): Promise<QualityStep[]> {
|
|
73
|
+
const typecheck = runQualityStep(project, ['--no-install', 'check', '--no-script', '--no-pretty'])
|
|
74
|
+
const format = formatProject(project, true)
|
|
75
|
+
return [{ name: 'typecheck', ...(await typecheck) }, await format]
|
|
61
76
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path'
|
|
2
|
+
import toolchain from '../../toolchain.json'
|
|
3
|
+
import { consumerScripts, consumerTsconfig, installedCli, json } from './consumer-files.ts'
|
|
4
|
+
import type { Framework } from './bundle.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The dependencies of a project created by the npm-installed CLI: every
|
|
8
|
+
* `@bun-native/*` package at the CLI's own release version, plus the framework and
|
|
9
|
+
* tool pins the release was qualified with. `scripts/registry-packages.ts` derives it
|
|
10
|
+
* from the package set's consumer definitions and publishes it in the CLI's
|
|
11
|
+
* `package.json` under `bunNative.templates`.
|
|
12
|
+
*/
|
|
13
|
+
export type RegistryTemplate = {
|
|
14
|
+
dependencies: Record<string, string>
|
|
15
|
+
devDependencies: Record<string, string>
|
|
16
|
+
}
|
|
17
|
+
export type RegistryTemplates = Record<Framework, RegistryTemplate>
|
|
18
|
+
|
|
19
|
+
type ConsumerPins = Record<Framework, RegistryTemplate>
|
|
20
|
+
|
|
21
|
+
/** The published templates for `version`: local `@bun-native/*` versions become the release version. */
|
|
22
|
+
export function registryTemplates(consumers: ConsumerPins, version: string): RegistryTemplates {
|
|
23
|
+
const release = (pins: Record<string, string>) =>
|
|
24
|
+
Object.fromEntries(
|
|
25
|
+
Object.entries(pins).map(([name, pin]) => [
|
|
26
|
+
name,
|
|
27
|
+
name.startsWith('@bun-native/') ? version : pin,
|
|
28
|
+
])
|
|
29
|
+
)
|
|
30
|
+
const template = (framework: Framework): RegistryTemplate => ({
|
|
31
|
+
dependencies: release(consumers[framework].dependencies),
|
|
32
|
+
devDependencies: release(consumers[framework].devDependencies),
|
|
33
|
+
})
|
|
34
|
+
return { solid: template('solid'), react: template('react') }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The templates an npm-installed CLI carries, read from its own `package.json`
|
|
39
|
+
* (`root` is the package's `src`). A CLI from a local package set has none.
|
|
40
|
+
*/
|
|
41
|
+
export async function installedTemplates(root: string): Promise<RegistryTemplates> {
|
|
42
|
+
const manifest: { bunNative?: { templates?: RegistryTemplates } } = await Bun.file(
|
|
43
|
+
join(dirname(root), 'package.json')
|
|
44
|
+
).json()
|
|
45
|
+
const templates = manifest.bunNative?.templates
|
|
46
|
+
if (!templates?.solid || !templates.react)
|
|
47
|
+
throw new Error(
|
|
48
|
+
'This CLI was not installed from the npm registry; create the project with --packages /path/to/package-set/manifest.json'
|
|
49
|
+
)
|
|
50
|
+
return templates
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registryFiles(
|
|
54
|
+
template: RegistryTemplate,
|
|
55
|
+
framework: Framework,
|
|
56
|
+
entry: string,
|
|
57
|
+
name: string
|
|
58
|
+
): Record<string, string> {
|
|
59
|
+
const version = template.dependencies['@bun-native/core']
|
|
60
|
+
return {
|
|
61
|
+
'bun-native.json': json({ schemaVersion: 1, framework, entry }),
|
|
62
|
+
'package.json': json({
|
|
63
|
+
name,
|
|
64
|
+
private: true,
|
|
65
|
+
type: 'module',
|
|
66
|
+
packageManager: `bun@${toolchain.bun.version}`,
|
|
67
|
+
engines: { bun: toolchain.bun.version },
|
|
68
|
+
scripts: consumerScripts(),
|
|
69
|
+
dependencies: template.dependencies,
|
|
70
|
+
devDependencies: template.devDependencies,
|
|
71
|
+
}),
|
|
72
|
+
'tsconfig.json': consumerTsconfig(framework, entry, ['dist', 'node_modules', '.bun-native']),
|
|
73
|
+
'.gitignore': 'dist/\n.bun-native/\nnode_modules/\n',
|
|
74
|
+
'README.md': `# ${name}
|
|
75
|
+
|
|
76
|
+
This project depends on the Bun Native ${version} packages from the npm registry. Use the Bun version in \`package.json\` \`engines\` (${toolchain.bun.cliRevision}); \`bun run build\` refuses any other compiler.
|
|
77
|
+
|
|
78
|
+
1. \`bun install\` installs the pinned dependencies, including this project's own \`@bun-native/cli\`.
|
|
79
|
+
2. \`${installedCli} host install\` downloads the released native host for this version into \`.bun-native/hosts\` and prints its path. Released hosts exist for linux-x64, linux-arm64, darwin-arm64 and darwin-x64.
|
|
80
|
+
|
|
81
|
+
- \`bun run check\` checks TypeScript or checked JavaScript.
|
|
82
|
+
- \`bun run format\` applies Bun's built-in formatter in the template style unless the project supplies its own formatter config; \`bun run format:check\` only reports differences.
|
|
83
|
+
- \`bun run lint\` runs the CLI's pinned \`oxlint\` with the template rules unless the project supplies its own \`.oxlintrc.json\`.
|
|
84
|
+
- \`bun run dev --host auto\` bundles and opens the app once with the released host (or pass \`--host /absolute/path/to/bun-native-host\`). Add \`--watch\` to remount after source edits; state resets on every edit.
|
|
85
|
+
- \`bun run build --host auto --host-profile release\` creates an unsigned same-platform AOT app in \`dist\`. The output directory must be empty.
|
|
86
|
+
|
|
87
|
+
Keep the template's optional development IPC hooks: they report readiness and clean up the renderer and owned host before a replacement starts. The host is unsigned; see the Bun Native [getting-started guide](https://github.com/0xPlayerOne/bun-native/blob/v${version}/docs/getting-started.md) and [developer tooling](https://github.com/0xPlayerOne/bun-native/blob/v${version}/docs/developer-tooling.md).
|
|
88
|
+
`,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -230,7 +230,10 @@ export class DevSession {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
private async replace(changedAt: number): Promise<RemountReport> {
|
|
233
|
-
|
|
233
|
+
// Remounts reuse the verification stamp; any change to a verified path re-verifies fully.
|
|
234
|
+
const project = await readProject(this.repository, this.projectDirectory, {
|
|
235
|
+
verification: 'stamp',
|
|
236
|
+
})
|
|
234
237
|
const directory = await mkdtemp(join(this.temporaryDirectory, 'generation-'))
|
|
235
238
|
let owner: OwnedProcess | undefined
|
|
236
239
|
const start = performance.now()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { constants } from 'node:fs'
|
|
2
2
|
import { copyFile, mkdir, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { dirname, join } from 'node:path'
|
|
4
|
-
import
|
|
4
|
+
import { consumerScripts, consumerTsconfig, json } from './consumer-files.ts'
|
|
5
5
|
import { seedConsumerLock } from './package-lock.ts'
|
|
6
6
|
import { consumerArchiveLock, readPackageSet, type ReadPackageSet } from './package-set.ts'
|
|
7
7
|
|
|
@@ -22,14 +22,6 @@ export function standaloneFiles(
|
|
|
22
22
|
)
|
|
23
23
|
const dependencies = local(consumer.dependencies)
|
|
24
24
|
const devDependencies = local(consumer.devDependencies)
|
|
25
|
-
const cli = 'bun --no-install ./node_modules/@bun-native/cli/src/scripts/native.ts'
|
|
26
|
-
const json = (value: unknown) => JSON.stringify(value, null, 2) + '\n'
|
|
27
|
-
// The template formatter keeps short string arrays on one line, so format checks pass as generated.
|
|
28
|
-
const formattedJSON = (value: unknown) =>
|
|
29
|
-
json(value).replace(
|
|
30
|
-
/\[\n\s*("[^"\n]*"(?:,\n\s*"[^"\n]*")*)\n\s*\]/g,
|
|
31
|
-
(_, items: string) => `[${items.split(/,\n\s*/).join(', ')}]`
|
|
32
|
-
)
|
|
33
25
|
return {
|
|
34
26
|
'bun-native.json': json({ schemaVersion: 1, framework, entry, packageSet: packageSetPath }),
|
|
35
27
|
'package.json': json({
|
|
@@ -38,16 +30,7 @@ export function standaloneFiles(
|
|
|
38
30
|
type: 'module',
|
|
39
31
|
packageManager: `bun@${set.manifest.toolchain.bun.version}`,
|
|
40
32
|
engines: { bun: set.manifest.toolchain.bun.version },
|
|
41
|
-
scripts:
|
|
42
|
-
// The persistent watch controller returns freed bundler pages promptly;
|
|
43
|
-
// see docs/developer-tooling.md "Controller memory".
|
|
44
|
-
dev: `MIMALLOC_PURGE_DELAY=0 ${cli} dev .`,
|
|
45
|
-
build: `${cli} build .`,
|
|
46
|
-
check: 'bun --no-install check --no-script --no-pretty',
|
|
47
|
-
format: `${cli} format .`,
|
|
48
|
-
'format:check': `${cli} format . --check`,
|
|
49
|
-
lint: `${cli} lint .`,
|
|
50
|
-
},
|
|
33
|
+
scripts: consumerScripts(),
|
|
51
34
|
dependencies,
|
|
52
35
|
devDependencies,
|
|
53
36
|
overrides,
|
|
@@ -55,14 +38,12 @@ export function standaloneFiles(
|
|
|
55
38
|
'bun.lock': json(
|
|
56
39
|
seedConsumerLock(consumer.lock, { name, dependencies, devDependencies }, localLock)
|
|
57
40
|
),
|
|
58
|
-
'tsconfig.json':
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
exclude: ['dist', 'node_modules', '.bun-native', 'vendor'],
|
|
65
|
-
}),
|
|
41
|
+
'tsconfig.json': consumerTsconfig(framework, entry, [
|
|
42
|
+
'dist',
|
|
43
|
+
'node_modules',
|
|
44
|
+
'.bun-native',
|
|
45
|
+
'vendor',
|
|
46
|
+
]),
|
|
66
47
|
'.gitignore': 'dist/\n.bun-native/\nnode_modules/\n',
|
|
67
48
|
'README.md': `# ${name}\n\nThis standalone project carries its local Bun Native package archives in \`vendor/bun-native\`. Commit that directory with this project. Its source snapshot is \`${set.manifest.source.commit}\`; use Bun revision \`${set.manifest.toolchain.bun.revision}\`. No checkout paths or workspace links are required.\n\nThe initial \`bun.lock\` contains every selected local archive tuple with SHA-256 integrity, matching local overrides, and the exact existing external dependency graph. No local root is left unresolved. First run \`bun install --offline --ignore-scripts --linker hoisted --backend copyfile\` with a populated package cache. An empty offline cache must fail; the local archives do not contain the third-party packages. Preserve the resulting lock, then use \`bun install --offline --frozen-lockfile --ignore-scripts --linker hoisted --backend copyfile\` for subsequent installs and relocation. The CLI never installs packages implicitly.\n\n- \`bun run check\` checks TypeScript or checked JavaScript.\n- \`bun run format\` applies Bun's built-in formatter in the template style unless the project supplies its own formatter config; \`bun run format:check\` only reports differences.\n- \`bun run lint\` runs the pinned \`oxlint\` with the CLI's template rules unless the project supplies its own \`.oxlintrc.json\`. It never lints \`vendor/\`, \`dist/\`, \`.bun-native/\` or \`node_modules/\`.\n- \`bun run dev --host /absolute/path/to/bun-native-host\` bundles and opens the app once.\n- Add \`--watch\` to remount after source edits with joined teardown. State resets; state-preserving hot reload is not implemented.\n- \`bun run build --host /absolute/path/to/bun-native-host --host-profile release\` creates an unsigned same-platform AOT app in \`dist\`. The output directory must be empty.\n\nSupply a separately acquired, compatible prebuilt native host. These commands never download it or run Cargo. Keep the template's optional development IPC hooks: they report readiness and join renderer/application/host cleanup. The archives are private experimental source packages, not a signed registry release. Their checksums establish consistency with the supplied manifest; verify that manifest's origin separately.\n\nSolid uses the universal JSX transform with \`jsx: preserve\`; its \`jsx-runtime\` export supplies types, not React-style automatic JSX functions. React uses its native JSX runtime. DOM/WebView APIs and browser component libraries are unsupported. See [the source-bound tooling documentation](https://github.com/0xPlayerOne/bun-native/blob/${set.manifest.source.commit}/docs/developer-tooling.md) for the native/platform and measurement limits.\n`,
|
|
68
49
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* USTAR header primitives shared by the package-archive reader, the host-archive
|
|
3
|
+
* reader and the host-archive writer. Each reader keeps its own member rules and
|
|
4
|
+
* error messages; scripts/install-bun/archive.mjs keeps its own copy because it
|
|
5
|
+
* must run under Node before Bun exists.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const USTAR_BLOCK = 512
|
|
9
|
+
|
|
10
|
+
/** A NUL/space-padded octal field as a safe integer, or undefined when malformed. */
|
|
11
|
+
export function ustarNumber(field: Uint8Array): number | undefined {
|
|
12
|
+
const text = Buffer.from(field.buffer, field.byteOffset, field.byteLength).toString('latin1')
|
|
13
|
+
if (!/^[\0 ]*[0-7]+[\0 ]*$/.test(text)) return undefined
|
|
14
|
+
const value = Number.parseInt(text.replaceAll('\0', '').trim(), 8)
|
|
15
|
+
return Number.isSafeInteger(value) ? value : undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The header checksum, counting the checksum field itself as eight spaces. */
|
|
19
|
+
export function ustarChecksum(header: Uint8Array): number {
|
|
20
|
+
let sum = 0
|
|
21
|
+
for (let index = 0; index < USTAR_BLOCK; index += 1)
|
|
22
|
+
sum += index >= 148 && index < 156 ? 0x20 : header[index]!
|
|
23
|
+
return sum
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether a header's stored checksum field matches its bytes. */
|
|
27
|
+
export function ustarChecksumValid(header: Uint8Array): boolean {
|
|
28
|
+
return ustarNumber(header.subarray(148, 156)) === ustarChecksum(header)
|
|
29
|
+
}
|