@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,220 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
|
+
import { packagePath } from './paths.ts'
|
|
3
|
+
|
|
4
|
+
type LockedPackage = [string, string, Record<string, unknown>, string]
|
|
5
|
+
export type DependencyLock = {
|
|
6
|
+
lockfileVersion: 2
|
|
7
|
+
configVersion: 1
|
|
8
|
+
packages: Record<string, LockedPackage>
|
|
9
|
+
}
|
|
10
|
+
type ArchiveInfo = { dependencies?: Record<string, string>; bin?: Record<string, string> }
|
|
11
|
+
type LocalArchiveLock = {
|
|
12
|
+
packages: Record<string, [string, ArchiveInfo, string]>
|
|
13
|
+
overrides: Record<string, string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Pinned Bun text locks use raw tarball paths; file: denotes a folder in this field. */
|
|
17
|
+
export function localArchiveLock(
|
|
18
|
+
archives: readonly {
|
|
19
|
+
name: string
|
|
20
|
+
file: string
|
|
21
|
+
sha256: string
|
|
22
|
+
dependencies?: Record<string, string>
|
|
23
|
+
bin?: Record<string, string>
|
|
24
|
+
}[]
|
|
25
|
+
): LocalArchiveLock {
|
|
26
|
+
const packages: LocalArchiveLock['packages'] = Object.create(null)
|
|
27
|
+
const overrides: Record<string, string> = Object.create(null)
|
|
28
|
+
for (const archive of archives) {
|
|
29
|
+
if (
|
|
30
|
+
!archive.name.startsWith('@bun-native/') ||
|
|
31
|
+
!packagePath(archive.file) ||
|
|
32
|
+
!archive.file.endsWith('.tgz') ||
|
|
33
|
+
!/^[a-f0-9]{64}$/.test(archive.sha256) ||
|
|
34
|
+
Object.hasOwn(packages, archive.name)
|
|
35
|
+
)
|
|
36
|
+
throw new Error(`Invalid or duplicate local archive lock input: ${archive.name}`)
|
|
37
|
+
const info: ArchiveInfo = {
|
|
38
|
+
...(archive.dependencies ? { dependencies: archive.dependencies } : {}),
|
|
39
|
+
...(archive.bin ? { bin: archive.bin } : {}),
|
|
40
|
+
}
|
|
41
|
+
packages[archive.name] = [
|
|
42
|
+
`${archive.name}@${archive.file}`,
|
|
43
|
+
info,
|
|
44
|
+
`sha256-${Buffer.from(archive.sha256, 'hex').toString('base64')}`,
|
|
45
|
+
]
|
|
46
|
+
overrides[archive.name] = `file:${archive.file}`
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
packages: Object.fromEntries(Object.entries(packages)),
|
|
50
|
+
overrides: Object.fromEntries(Object.entries(overrides)),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function record(value: unknown): Record<string, unknown> {
|
|
55
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
56
|
+
throw new Error('Invalid dependency lock record')
|
|
57
|
+
return value as Record<string, unknown>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function lockedPackages(value: unknown, local?: LocalArchiveLock): DependencyLock {
|
|
61
|
+
const lock = record(value)
|
|
62
|
+
if (lock['lockfileVersion'] !== 2 || lock['configVersion'] !== 1)
|
|
63
|
+
throw new Error('Expected the pinned Bun dependency lock format')
|
|
64
|
+
const supplied = record(lock['packages'])
|
|
65
|
+
if (local)
|
|
66
|
+
for (const [name, expected] of Object.entries(local.packages))
|
|
67
|
+
if (!Object.hasOwn(supplied, name) || !isDeepStrictEqual(supplied[name], expected))
|
|
68
|
+
throw new Error(`Installed local archive lock differs: ${name}`)
|
|
69
|
+
const packages: DependencyLock['packages'] = Object.create(null)
|
|
70
|
+
for (const [key, value] of Object.entries(supplied)) {
|
|
71
|
+
if (local && Object.hasOwn(local.packages, key)) continue
|
|
72
|
+
if (!Array.isArray(value) || typeof value[0] !== 'string')
|
|
73
|
+
throw new Error(`Invalid locked package: ${key}`)
|
|
74
|
+
if (value[0].includes('@workspace:') || value[0].includes('@file:')) {
|
|
75
|
+
if (local || !key.startsWith('@bun-native/') || value[0].includes('@file:'))
|
|
76
|
+
throw new Error(`Unexpected local dependency in lock: ${key}`)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
value.length !== 4 ||
|
|
81
|
+
value[1] !== '' ||
|
|
82
|
+
typeof value[3] !== 'string' ||
|
|
83
|
+
!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(value[3])
|
|
84
|
+
)
|
|
85
|
+
throw new Error(`Expected an integrity-bound registry dependency: ${key}`)
|
|
86
|
+
packages[key] = [value[0], value[1], record(value[2]), value[3]]
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
lockfileVersion: 2,
|
|
90
|
+
configVersion: 1,
|
|
91
|
+
packages: Object.fromEntries(Object.entries(packages)),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function identity(entry: LockedPackage): { name: string; version: string } {
|
|
96
|
+
const separator = entry[0].lastIndexOf('@')
|
|
97
|
+
const name = entry[0].slice(0, separator)
|
|
98
|
+
const version = entry[0].slice(separator + 1)
|
|
99
|
+
if (separator <= 0 || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
|
|
100
|
+
throw new Error(`Expected an exact registry version: ${entry[0]}`)
|
|
101
|
+
return { name, version }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Preserve nested resolutions; a global name override would collapse distinct versions. */
|
|
105
|
+
export function dependencyClosure(
|
|
106
|
+
lock: DependencyLock,
|
|
107
|
+
roots: Record<string, string>,
|
|
108
|
+
includeInstalledOptionalPeers = false
|
|
109
|
+
): DependencyLock {
|
|
110
|
+
const packages: DependencyLock['packages'] = Object.create(null)
|
|
111
|
+
const pending: string[] = []
|
|
112
|
+
for (const [name, version] of Object.entries(roots)) {
|
|
113
|
+
const entry = Object.hasOwn(lock.packages, name) ? lock.packages[name] : undefined
|
|
114
|
+
if (!entry || identity(entry).name !== name || identity(entry).version !== version)
|
|
115
|
+
throw new Error(`Source lock does not pin ${name}@${version}`)
|
|
116
|
+
pending.push(name)
|
|
117
|
+
}
|
|
118
|
+
const resolve = (from: string, name: string, required: boolean): string | undefined => {
|
|
119
|
+
let ancestor = from
|
|
120
|
+
while (ancestor) {
|
|
121
|
+
const candidate = `${ancestor}/${name}`
|
|
122
|
+
if (Object.hasOwn(lock.packages, candidate)) return candidate
|
|
123
|
+
const current = Object.hasOwn(lock.packages, ancestor) ? lock.packages[ancestor] : undefined
|
|
124
|
+
if (!current || !ancestor.endsWith(identity(current).name))
|
|
125
|
+
throw new Error(`Invalid nested dependency locator: ${ancestor}`)
|
|
126
|
+
ancestor = ancestor.slice(0, -identity(current).name.length).replace(/\/$/, '')
|
|
127
|
+
}
|
|
128
|
+
if (Object.hasOwn(lock.packages, name)) return name
|
|
129
|
+
if (required) throw new Error(`Source lock is missing ${name}, required by ${from}`)
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
133
|
+
const key = pending[index]!
|
|
134
|
+
if (packages[key]) continue
|
|
135
|
+
const entry = lock.packages[key]!
|
|
136
|
+
identity(entry)
|
|
137
|
+
packages[key] = entry
|
|
138
|
+
const optionalPeers = entry[2]['optionalPeers'] ?? []
|
|
139
|
+
if (!Array.isArray(optionalPeers) || optionalPeers.some((name) => typeof name !== 'string'))
|
|
140
|
+
throw new Error(`Invalid optional peer metadata: ${key}`)
|
|
141
|
+
for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
142
|
+
const dependencies = entry[2][field]
|
|
143
|
+
if (dependencies === undefined) continue
|
|
144
|
+
for (const [name, version] of Object.entries(record(dependencies))) {
|
|
145
|
+
if (typeof version !== 'string') throw new Error(`Invalid ${key} dependency ${name}`)
|
|
146
|
+
// Optional peer declarations remain byte-identical in the retained
|
|
147
|
+
// tuple. They do not add an installation root that the source lacked.
|
|
148
|
+
const optionalPeer = field === 'peerDependencies' && optionalPeers.includes(name)
|
|
149
|
+
if (optionalPeer && !includeInstalledOptionalPeers) continue
|
|
150
|
+
const target = resolve(key, name, !optionalPeer && field !== 'optionalDependencies')
|
|
151
|
+
if (target) pending.push(target)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
lockfileVersion: 2,
|
|
157
|
+
configVersion: 1,
|
|
158
|
+
packages: Object.fromEntries(
|
|
159
|
+
Object.entries(packages).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
160
|
+
),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Keep a complete lock graph; Bun rejects missing local roots before installation. */
|
|
165
|
+
export function seedConsumerLock(
|
|
166
|
+
lock: DependencyLock,
|
|
167
|
+
manifest: {
|
|
168
|
+
name: string
|
|
169
|
+
dependencies: Record<string, string>
|
|
170
|
+
devDependencies: Record<string, string>
|
|
171
|
+
},
|
|
172
|
+
local: LocalArchiveLock = { packages: {}, overrides: {} }
|
|
173
|
+
) {
|
|
174
|
+
const packages = { ...lock.packages, ...local.packages }
|
|
175
|
+
for (const name of Object.keys({ ...manifest.dependencies, ...manifest.devDependencies }))
|
|
176
|
+
if (!Object.hasOwn(packages, name))
|
|
177
|
+
throw new Error(`Consumer lock is missing a root package: ${name}`)
|
|
178
|
+
return {
|
|
179
|
+
lockfileVersion: lock.lockfileVersion,
|
|
180
|
+
configVersion: lock.configVersion,
|
|
181
|
+
workspaces: { '': manifest },
|
|
182
|
+
overrides: local.overrides,
|
|
183
|
+
packages,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** No new tuple, changed version/integrity/edge, or omitted required nested tuple is accepted. */
|
|
188
|
+
export function verifyConsumerLock(
|
|
189
|
+
value: unknown,
|
|
190
|
+
expected: DependencyLock,
|
|
191
|
+
allowAdditional = false,
|
|
192
|
+
local?: LocalArchiveLock
|
|
193
|
+
): void {
|
|
194
|
+
// Applications may add unrelated dependencies. The fixed qualification cohort
|
|
195
|
+
// uses the exact comparison; runtime validation still pins every required tuple.
|
|
196
|
+
const packages = record(record(value)['packages'])
|
|
197
|
+
const localRoots = Object.keys(local?.packages ?? {})
|
|
198
|
+
if (Object.keys(packages).some((key) => localRoots.some((root) => key.startsWith(root + '/'))))
|
|
199
|
+
throw new Error(
|
|
200
|
+
'Installed dependency lock introduces a nested locator beneath a required local package'
|
|
201
|
+
)
|
|
202
|
+
const supplied = lockedPackages(value, local)
|
|
203
|
+
const roots = Object.fromEntries(
|
|
204
|
+
Object.entries(expected.packages)
|
|
205
|
+
.filter(([key, entry]) => key === identity(entry).name)
|
|
206
|
+
.map(([key, entry]) => [key, identity(entry).version])
|
|
207
|
+
)
|
|
208
|
+
const actual = allowAdditional ? dependencyClosure(supplied, roots, true) : supplied
|
|
209
|
+
if (!isDeepStrictEqual(actual, expected))
|
|
210
|
+
throw new Error('Installed dependency lock differs from the supplied pinned package set')
|
|
211
|
+
if (local) {
|
|
212
|
+
const overrides = record(record(value)['overrides'])
|
|
213
|
+
// Selector/parent override forms can affect required packages too. Keep the
|
|
214
|
+
// complete override map exact, while permitting unrelated registry roots.
|
|
215
|
+
if (!isDeepStrictEqual(overrides, local.overrides))
|
|
216
|
+
throw new Error(
|
|
217
|
+
'Installed local archive overrides differ from the supplied pinned package set'
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { readdir, realpath, stat } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
4
|
+
import toolchain from '../../toolchain.json'
|
|
5
|
+
import {
|
|
6
|
+
dependencyClosure,
|
|
7
|
+
lockedPackages,
|
|
8
|
+
localArchiveLock,
|
|
9
|
+
verifyConsumerLock,
|
|
10
|
+
type DependencyLock,
|
|
11
|
+
} from './package-lock.ts'
|
|
12
|
+
import { inside, packageArchivePath, packagePath } from './paths.ts'
|
|
13
|
+
import { fileDigest, sha256, type FileDigest } from './artifact.ts'
|
|
14
|
+
import { verifyPackageArchive } from './package-archive.ts'
|
|
15
|
+
|
|
16
|
+
export const packageNames = [
|
|
17
|
+
'@bun-native/cli',
|
|
18
|
+
'@bun-native/core',
|
|
19
|
+
'@bun-native/protocol',
|
|
20
|
+
'@bun-native/react',
|
|
21
|
+
'@bun-native/renderer',
|
|
22
|
+
'@bun-native/runtime',
|
|
23
|
+
'@bun-native/solid',
|
|
24
|
+
] as const
|
|
25
|
+
|
|
26
|
+
export type PackageManifest = {
|
|
27
|
+
name: string
|
|
28
|
+
version: string
|
|
29
|
+
private: true
|
|
30
|
+
type: 'module'
|
|
31
|
+
license: 'MIT'
|
|
32
|
+
files: string[]
|
|
33
|
+
engines: { bun: string }
|
|
34
|
+
dependencies?: Record<string, string>
|
|
35
|
+
exports?: string | Record<string, string>
|
|
36
|
+
bin?: Record<string, string>
|
|
37
|
+
}
|
|
38
|
+
export type LocalPackage = {
|
|
39
|
+
manifest: PackageManifest
|
|
40
|
+
archive: FileDigest & { file: string }
|
|
41
|
+
files: Record<string, FileDigest>
|
|
42
|
+
}
|
|
43
|
+
export type Consumer = {
|
|
44
|
+
dependencies: Record<string, string>
|
|
45
|
+
devDependencies: Record<string, string>
|
|
46
|
+
lock: DependencyLock
|
|
47
|
+
}
|
|
48
|
+
export type PackageSet = {
|
|
49
|
+
schemaVersion: 1
|
|
50
|
+
source: { commit: string; tree: string; files: Record<string, FileDigest> }
|
|
51
|
+
toolchain: typeof toolchain
|
|
52
|
+
compiler: { revision: string; os: string; architecture: string; executable: FileDigest }
|
|
53
|
+
packages: Record<string, LocalPackage>
|
|
54
|
+
consumers: { solid: Consumer; react: Consumer }
|
|
55
|
+
}
|
|
56
|
+
export type ReadPackageSet = { directory: string; manifest: PackageSet; text: string }
|
|
57
|
+
|
|
58
|
+
function object(value: unknown): Record<string, unknown> {
|
|
59
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
60
|
+
throw new Error('Invalid package-set object')
|
|
61
|
+
return value as Record<string, unknown>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function keys(value: Record<string, unknown>, required: string[], optional: string[] = []): void {
|
|
65
|
+
if (
|
|
66
|
+
required.some((key) => !Object.hasOwn(value, key)) ||
|
|
67
|
+
Object.keys(value).some((key) => !required.includes(key) && !optional.includes(key))
|
|
68
|
+
)
|
|
69
|
+
throw new Error('Unexpected or missing package-set fields')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function digest(value: unknown): FileDigest {
|
|
73
|
+
const fields = object(value)
|
|
74
|
+
if (
|
|
75
|
+
!Number.isSafeInteger(fields['bytes']) ||
|
|
76
|
+
(fields['bytes'] as number) < 0 ||
|
|
77
|
+
typeof fields['sha256'] !== 'string' ||
|
|
78
|
+
!/^[a-f0-9]{64}$/.test(fields['sha256'])
|
|
79
|
+
)
|
|
80
|
+
throw new Error('Invalid package-set file digest')
|
|
81
|
+
return { bytes: fields['bytes'] as number, sha256: fields['sha256'] }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function files(value: unknown): Record<string, FileDigest> {
|
|
85
|
+
const result: Record<string, FileDigest> = Object.create(null)
|
|
86
|
+
for (const [path, entry] of Object.entries(object(value))) {
|
|
87
|
+
if (!packagePath(path)) throw new Error(`Invalid package-set file path: ${path}`)
|
|
88
|
+
keys(object(entry), ['bytes', 'sha256'])
|
|
89
|
+
result[path] = digest(entry)
|
|
90
|
+
}
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function versions(value: unknown): Record<string, string> {
|
|
95
|
+
const result: Record<string, string> = Object.create(null)
|
|
96
|
+
for (const [name, version] of Object.entries(object(value))) {
|
|
97
|
+
if (
|
|
98
|
+
!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(name) ||
|
|
99
|
+
typeof version !== 'string' ||
|
|
100
|
+
!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)
|
|
101
|
+
)
|
|
102
|
+
throw new Error(`Package-set dependency must have an exact version: ${name}`)
|
|
103
|
+
result[name] = version
|
|
104
|
+
}
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function exportedPaths(value: unknown): string[] {
|
|
109
|
+
return typeof value === 'string'
|
|
110
|
+
? [value]
|
|
111
|
+
: Object.values(object(value)).map((entry) => {
|
|
112
|
+
if (typeof entry !== 'string') throw new Error('Invalid package export/bin path')
|
|
113
|
+
return entry
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function packageManifest(
|
|
118
|
+
value: unknown,
|
|
119
|
+
name: string,
|
|
120
|
+
contents: Record<string, FileDigest>
|
|
121
|
+
): PackageManifest {
|
|
122
|
+
const manifest = object(value)
|
|
123
|
+
keys(
|
|
124
|
+
manifest,
|
|
125
|
+
['name', 'version', 'private', 'type', 'license', 'files', 'engines'],
|
|
126
|
+
['dependencies', 'exports', 'bin']
|
|
127
|
+
)
|
|
128
|
+
if (
|
|
129
|
+
manifest['name'] !== name ||
|
|
130
|
+
manifest['private'] !== true ||
|
|
131
|
+
manifest['type'] !== 'module' ||
|
|
132
|
+
manifest['license'] !== 'MIT' ||
|
|
133
|
+
!isDeepStrictEqual(manifest['engines'], { bun: toolchain.bun.version }) ||
|
|
134
|
+
!Array.isArray(manifest['files']) ||
|
|
135
|
+
manifest['files'].some((path) => !packageArchivePath(path))
|
|
136
|
+
)
|
|
137
|
+
throw new Error(`Invalid materialized package manifest: ${name}`)
|
|
138
|
+
versions({ [name]: manifest['version'] })
|
|
139
|
+
if (manifest['dependencies'] !== undefined) versions(manifest['dependencies'])
|
|
140
|
+
for (const field of ['exports', 'bin']) {
|
|
141
|
+
if (manifest[field] === undefined) continue
|
|
142
|
+
for (const exported of exportedPaths(manifest[field])) {
|
|
143
|
+
const path = exported.replace(/^\.\//, '')
|
|
144
|
+
if (!packagePath(path) || !Object.hasOwn(contents, path))
|
|
145
|
+
throw new Error(`Unpackaged ${field} target: ${name} ${exported}`)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
!Object.hasOwn(contents, 'LICENSE') ||
|
|
150
|
+
!Object.hasOwn(contents, 'bun-native-source.json') ||
|
|
151
|
+
!Object.hasOwn(contents, 'package.json') ||
|
|
152
|
+
(name === '@bun-native/cli'
|
|
153
|
+
? !isDeepStrictEqual(manifest['bin'], { 'bun-native': './src/scripts/native.ts' }) ||
|
|
154
|
+
manifest['exports'] !== undefined
|
|
155
|
+
: manifest['exports'] === undefined || manifest['bin'] !== undefined)
|
|
156
|
+
)
|
|
157
|
+
throw new Error(`Missing package entry/license/provenance: ${name}`)
|
|
158
|
+
const text = JSON.stringify(manifest, null, 2) + '\n'
|
|
159
|
+
if (
|
|
160
|
+
!isDeepStrictEqual(contents['package.json'], {
|
|
161
|
+
bytes: Buffer.byteLength(text),
|
|
162
|
+
sha256: sha256(text),
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
throw new Error(`Package manifest digest mismatch: ${name}`)
|
|
166
|
+
return manifest as PackageManifest
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function requiredPackages(set: PackageSet, framework: 'solid' | 'react'): string[] {
|
|
170
|
+
const pending = ['@bun-native/cli', '@bun-native/core', `@bun-native/${framework}`]
|
|
171
|
+
const required = new Set<string>()
|
|
172
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
173
|
+
const name = pending[index]!
|
|
174
|
+
if (required.has(name)) continue
|
|
175
|
+
const pkg = set.packages[name]
|
|
176
|
+
if (!pkg) throw new Error(`Missing local package: ${name}`)
|
|
177
|
+
required.add(name)
|
|
178
|
+
for (const [dependency, version] of Object.entries(pkg.manifest.dependencies ?? {})) {
|
|
179
|
+
if (!dependency.startsWith('@bun-native/')) continue
|
|
180
|
+
if (set.packages[dependency]?.manifest.version !== version)
|
|
181
|
+
throw new Error(`Inconsistent internal dependency: ${name} -> ${dependency}`)
|
|
182
|
+
pending.push(dependency)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return [...required].sort()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function consumerArchiveLock(
|
|
189
|
+
set: PackageSet,
|
|
190
|
+
framework: 'solid' | 'react',
|
|
191
|
+
archiveDirectory = 'vendor/bun-native'
|
|
192
|
+
) {
|
|
193
|
+
return localArchiveLock(
|
|
194
|
+
requiredPackages(set, framework).map((name) => {
|
|
195
|
+
const pkg = set.packages[name]!
|
|
196
|
+
return {
|
|
197
|
+
name,
|
|
198
|
+
file: `${archiveDirectory ? archiveDirectory + '/' : ''}${pkg.archive.file}`,
|
|
199
|
+
sha256: pkg.archive.sha256,
|
|
200
|
+
...(pkg.manifest.dependencies ? { dependencies: pkg.manifest.dependencies } : {}),
|
|
201
|
+
...(pkg.manifest.bin ? { bin: pkg.manifest.bin } : {}),
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Validates data only; archives are hashed separately before any destination is written. */
|
|
208
|
+
export function parsePackageSet(value: unknown): PackageSet {
|
|
209
|
+
const fields = object(value)
|
|
210
|
+
keys(fields, ['schemaVersion', 'source', 'toolchain', 'compiler', 'packages', 'consumers'])
|
|
211
|
+
if (fields['schemaVersion'] !== 1 || !isDeepStrictEqual(fields['toolchain'], toolchain))
|
|
212
|
+
throw new Error('Package set requires the exact pinned Bun Native toolchain')
|
|
213
|
+
const source = object(fields['source'])
|
|
214
|
+
keys(source, ['commit', 'tree', 'files'])
|
|
215
|
+
if (
|
|
216
|
+
![source['commit'], source['tree']].every(
|
|
217
|
+
(value) => typeof value === 'string' && /^[a-f0-9]{40}$/.test(value)
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
throw new Error('Package set requires exact source commit and tree identities')
|
|
221
|
+
files(source['files'])
|
|
222
|
+
const compiler = object(fields['compiler'])
|
|
223
|
+
keys(compiler, ['revision', 'os', 'architecture', 'executable'])
|
|
224
|
+
if (
|
|
225
|
+
compiler['revision'] !== toolchain.bun.revision ||
|
|
226
|
+
typeof compiler['os'] !== 'string' ||
|
|
227
|
+
typeof compiler['architecture'] !== 'string'
|
|
228
|
+
)
|
|
229
|
+
throw new Error('Invalid package producer compiler identity')
|
|
230
|
+
digest(compiler['executable'])
|
|
231
|
+
const packages = object(fields['packages'])
|
|
232
|
+
keys(packages, [...packageNames])
|
|
233
|
+
const archives = new Set<string>()
|
|
234
|
+
for (const [name, entry] of Object.entries(packages)) {
|
|
235
|
+
const pkg = object(entry)
|
|
236
|
+
keys(pkg, ['manifest', 'archive', 'files'])
|
|
237
|
+
const contents = files(pkg['files'])
|
|
238
|
+
if (Object.keys(contents).some((path) => !packageArchivePath(path)))
|
|
239
|
+
throw new Error(`Source archive contains reserved installer metadata: ${name}`)
|
|
240
|
+
packageManifest(pkg['manifest'], name, contents)
|
|
241
|
+
const archive = object(pkg['archive'])
|
|
242
|
+
keys(archive, ['file', 'bytes', 'sha256'])
|
|
243
|
+
digest(archive)
|
|
244
|
+
if (
|
|
245
|
+
!packagePath(archive['file']) ||
|
|
246
|
+
!/^archives\/[a-z0-9._-]+\.tgz$/.test(archive['file']) ||
|
|
247
|
+
archives.has(archive['file'])
|
|
248
|
+
)
|
|
249
|
+
throw new Error('Package archives must have unique contained relative paths')
|
|
250
|
+
archives.add(archive['file'])
|
|
251
|
+
}
|
|
252
|
+
const consumers = object(fields['consumers'])
|
|
253
|
+
keys(consumers, ['solid', 'react'])
|
|
254
|
+
const set = fields as PackageSet
|
|
255
|
+
for (const framework of ['solid', 'react'] as const) {
|
|
256
|
+
const consumer = object(consumers[framework])
|
|
257
|
+
keys(consumer, ['dependencies', 'devDependencies', 'lock'])
|
|
258
|
+
const runtime = versions(consumer['dependencies'])
|
|
259
|
+
const development = versions(consumer['devDependencies'])
|
|
260
|
+
if (
|
|
261
|
+
!runtime['@bun-native/core'] ||
|
|
262
|
+
!runtime[`@bun-native/${framework}`] ||
|
|
263
|
+
!runtime[framework === 'solid' ? 'solid-js' : 'react'] ||
|
|
264
|
+
!development['@bun-native/cli'] ||
|
|
265
|
+
!development['@types/bun'] ||
|
|
266
|
+
(framework === 'react' &&
|
|
267
|
+
(!development['@types/react'] || !development['@types/react-reconciler']))
|
|
268
|
+
)
|
|
269
|
+
throw new Error(`Missing required consumer dependencies for ${framework}`)
|
|
270
|
+
const dependencies = { ...runtime, ...development }
|
|
271
|
+
const external: Record<string, string> = Object.create(null)
|
|
272
|
+
for (const name of requiredPackages(set, framework)) {
|
|
273
|
+
for (const [dependency, version] of Object.entries(
|
|
274
|
+
set.packages[name]!.manifest.dependencies ?? {}
|
|
275
|
+
)) {
|
|
276
|
+
if (dependencies[dependency] !== undefined && dependencies[dependency] !== version)
|
|
277
|
+
throw new Error(`Inconsistent consumer dependency: ${dependency}`)
|
|
278
|
+
dependencies[dependency] = version
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
282
|
+
if (name.startsWith('@bun-native/')) {
|
|
283
|
+
if (set.packages[name]?.manifest.version !== version)
|
|
284
|
+
throw new Error(`Unknown local dependency: ${name}`)
|
|
285
|
+
} else external[name] = version
|
|
286
|
+
}
|
|
287
|
+
const lock = lockedPackages(consumer['lock'])
|
|
288
|
+
if (
|
|
289
|
+
!isDeepStrictEqual(consumer['lock'], lock) ||
|
|
290
|
+
!isDeepStrictEqual(lock, dependencyClosure(lock, external))
|
|
291
|
+
)
|
|
292
|
+
throw new Error(`Unexpected dependency-lock entries for ${framework}`)
|
|
293
|
+
}
|
|
294
|
+
return set
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function containedFile(directory: string, path: string): Promise<string> {
|
|
298
|
+
const target = await realpath(join(directory, path))
|
|
299
|
+
if (!inside(directory, target) || !(await stat(target)).isFile())
|
|
300
|
+
throw new Error(`Package file escapes its owned directory or is not a file: ${path}`)
|
|
301
|
+
return target
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Run checks concurrently, but report the first failure in their listed order. */
|
|
305
|
+
async function settledInOrder(tasks: Promise<void>[]): Promise<void> {
|
|
306
|
+
for (const result of await Promise.allSettled(tasks))
|
|
307
|
+
if (result.status === 'rejected') throw result.reason
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function readPackageSet(path: string): Promise<ReadPackageSet> {
|
|
311
|
+
if (Bun.revision !== toolchain.bun.revision)
|
|
312
|
+
throw new Error('Use the pinned Bun for this package set')
|
|
313
|
+
const filename = await realpath(path)
|
|
314
|
+
const directory = dirname(filename)
|
|
315
|
+
const text = await Bun.file(filename).text()
|
|
316
|
+
const manifest = parsePackageSet(JSON.parse(text))
|
|
317
|
+
if (text !== JSON.stringify(manifest, null, 2) + '\n')
|
|
318
|
+
throw new Error('Package-set manifest must use its original canonical JSON encoding')
|
|
319
|
+
await settledInOrder(
|
|
320
|
+
Object.entries(manifest.packages).map(async ([name, pkg]) => {
|
|
321
|
+
const file = await containedFile(directory, pkg.archive.file)
|
|
322
|
+
if (!isDeepStrictEqual(await fileDigest(file), digest(pkg.archive)))
|
|
323
|
+
throw new Error(`Package archive digest mismatch: ${name}`)
|
|
324
|
+
await verifyPackageArchive(file, pkg.files)
|
|
325
|
+
})
|
|
326
|
+
)
|
|
327
|
+
return { directory, manifest, text }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Validate the installed CLI too, before its lazy compiler/watch modules are imported. */
|
|
331
|
+
export async function verifyInstalledPackages(
|
|
332
|
+
directory: string,
|
|
333
|
+
set: PackageSet,
|
|
334
|
+
framework: 'solid' | 'react',
|
|
335
|
+
archiveDirectory = 'vendor/bun-native'
|
|
336
|
+
): Promise<void> {
|
|
337
|
+
let modules: string
|
|
338
|
+
try {
|
|
339
|
+
modules = await realpath(join(directory, 'node_modules'))
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
342
|
+
throw new Error(
|
|
343
|
+
"Standalone dependencies are not installed; follow this project's pinned offline-install instructions"
|
|
344
|
+
)
|
|
345
|
+
throw error
|
|
346
|
+
}
|
|
347
|
+
if (!inside(directory, modules))
|
|
348
|
+
throw new Error('Installed packages must belong to this consumer')
|
|
349
|
+
const required = requiredPackages(set, framework)
|
|
350
|
+
verifyConsumerLock(
|
|
351
|
+
Bun.JSONC.parse(await Bun.file(join(directory, 'bun.lock')).text()),
|
|
352
|
+
set.consumers[framework].lock,
|
|
353
|
+
true,
|
|
354
|
+
consumerArchiveLock(set, framework, archiveDirectory)
|
|
355
|
+
)
|
|
356
|
+
const locations = new Map<string, string>()
|
|
357
|
+
for (const name of required) {
|
|
358
|
+
const pkg = set.packages[name]!
|
|
359
|
+
const root = await realpath(join(modules, name))
|
|
360
|
+
if (!inside(modules, root))
|
|
361
|
+
throw new Error(`Installed package resolves outside this consumer: ${name}`)
|
|
362
|
+
locations.set(name, root)
|
|
363
|
+
if (Object.keys(pkg.files).some((path) => !packageArchivePath(path)))
|
|
364
|
+
throw new Error(`Source archive contains reserved installer metadata: ${name}`)
|
|
365
|
+
// Pinned Integrity::Display uses unpadded base64 followed by "==" for
|
|
366
|
+
// SHA-256. This is its 52-byte installer tag, not the canonical lock SRI.
|
|
367
|
+
const tag = `sha256-${Buffer.from(pkg.archive.sha256, 'hex').toString('base64').replace(/=+$/, '')}==`
|
|
368
|
+
const installedFiles: Record<string, FileDigest> = {
|
|
369
|
+
...pkg.files,
|
|
370
|
+
'.bun-tag': {
|
|
371
|
+
bytes: Buffer.byteLength(tag),
|
|
372
|
+
sha256: sha256(tag),
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
const expected = Object.keys(installedFiles).sort()
|
|
376
|
+
const folders = new Set(
|
|
377
|
+
expected.flatMap((path) => {
|
|
378
|
+
const parts = path.split('/')
|
|
379
|
+
return parts.slice(1).map((_, index) => parts.slice(0, index + 1).join('/'))
|
|
380
|
+
})
|
|
381
|
+
)
|
|
382
|
+
const inventory: string[] = []
|
|
383
|
+
const visit = async (directory: string, prefix: string): Promise<void> => {
|
|
384
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
385
|
+
const path = prefix + entry.name
|
|
386
|
+
if (entry.isDirectory() && folders.has(path))
|
|
387
|
+
await visit(join(directory, entry.name), path + '/')
|
|
388
|
+
else if (entry.isFile() && Object.hasOwn(installedFiles, path)) inventory.push(path)
|
|
389
|
+
else throw new Error(`Installed package file inventory differs: ${name}/${path}`)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
await visit(root, '')
|
|
393
|
+
if (!isDeepStrictEqual(inventory.sort(), expected))
|
|
394
|
+
throw new Error(`Installed package file inventory differs: ${name}`)
|
|
395
|
+
await settledInOrder(
|
|
396
|
+
Object.entries(installedFiles).map(async ([path, expected]) => {
|
|
397
|
+
const file = await containedFile(root, path)
|
|
398
|
+
if (!isDeepStrictEqual(await fileDigest(file), expected))
|
|
399
|
+
throw new Error(`Installed package digest mismatch: ${name}/${path}`)
|
|
400
|
+
})
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
for (const name of required) {
|
|
404
|
+
const root = locations.get(name)!
|
|
405
|
+
for (const dependency of Object.keys(set.packages[name]!.manifest.dependencies ?? {})) {
|
|
406
|
+
if (!dependency.startsWith('@bun-native/')) continue
|
|
407
|
+
const expected = locations.get(dependency)!
|
|
408
|
+
const resolved = await realpath(Bun.resolveSync(dependency, root))
|
|
409
|
+
const exported = set.packages[dependency]!.manifest.exports!
|
|
410
|
+
const entry = typeof exported === 'string' ? exported : exported['.']!
|
|
411
|
+
if (resolved !== (await realpath(join(expected, entry))))
|
|
412
|
+
throw new Error(
|
|
413
|
+
`Installed dependency resolves to a different package: ${name} -> ${dependency}`
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|