@napi-rs/cli 3.7.4 → 3.8.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 +33 -0
- package/cli.mjs +9 -12
- package/dist/cli.js +9249 -1312
- package/dist/index.cjs +9269 -1332
- package/dist/index.d.cts +175 -157
- package/dist/index.d.ts +175 -157
- package/dist/index.js +9249 -1312
- package/docs/artifacts.md +33 -0
- package/docs/build.md +58 -0
- package/docs/create-npm-dirs.md +32 -0
- package/docs/new.md +39 -0
- package/docs/pre-publish.md +37 -0
- package/docs/rename.md +37 -0
- package/docs/universalize.md +31 -0
- package/docs/version.md +31 -0
- package/docs/wasi.md +161 -0
- package/package.json +11 -10
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +1019 -114
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/artifacts.spec.ts +17 -0
- package/src/api/__tests__/create-npm-dirs.spec.ts +60 -15
- package/src/api/__tests__/new.spec.ts +41 -5
- package/src/api/artifacts.ts +934 -108
- package/src/api/build.ts +1317 -188
- package/src/api/create-npm-dirs.ts +475 -102
- package/src/api/new.ts +354 -35
- package/src/api/pre-publish.ts +2845 -99
- package/src/api/rename.ts +1638 -124
- package/src/api/templates/js-binding.ts +216 -38
- package/src/api/templates/load-wasi-template.ts +1641 -111
- package/src/api/templates/wasi-worker-template.ts +32 -3
- package/src/commands/rename.ts +8 -3
- package/src/def/artifacts.ts +2 -2
- package/src/utils/__tests__/__snapshots__/target.spec.ts.md +2 -2
- package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
- package/src/utils/__tests__/misc.spec.ts +124 -3
- package/src/utils/__tests__/version.spec.ts +47 -1
- package/src/utils/config.ts +15 -2
- package/src/utils/metadata.ts +155 -47
- package/src/utils/misc.ts +6079 -0
- package/src/utils/target.ts +66 -8
- package/src/utils/typegen.ts +1223 -63
- package/src/utils/version.ts +96 -0
package/src/api/artifacts.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import {
|
|
4
|
+
dirname,
|
|
5
|
+
isAbsolute,
|
|
6
|
+
join,
|
|
7
|
+
parse,
|
|
8
|
+
relative,
|
|
9
|
+
resolve,
|
|
10
|
+
sep,
|
|
11
|
+
} from 'node:path'
|
|
2
12
|
|
|
3
13
|
import * as colors from 'colorette'
|
|
4
14
|
|
|
@@ -7,142 +17,910 @@ import {
|
|
|
7
17
|
type ArtifactsOptions,
|
|
8
18
|
} from '../def/artifacts.js'
|
|
9
19
|
import {
|
|
10
|
-
|
|
20
|
+
AVAILABLE_TARGETS,
|
|
21
|
+
commitFileSystemTransaction,
|
|
11
22
|
debugFactory,
|
|
23
|
+
fileExists,
|
|
24
|
+
parseTriple,
|
|
12
25
|
readFileAsync,
|
|
13
|
-
|
|
14
|
-
UniArchsByPlatform,
|
|
26
|
+
readNapiConfig,
|
|
15
27
|
readdirAsync,
|
|
28
|
+
resolvePackageReconciliationPaths,
|
|
29
|
+
type Target,
|
|
30
|
+
UniArchsByPlatform,
|
|
31
|
+
wasiLoaderSuffix,
|
|
32
|
+
wasiTargetHasThreads,
|
|
33
|
+
withFileSystemReconciliation,
|
|
16
34
|
} from '../utils/index.js'
|
|
35
|
+
import {
|
|
36
|
+
createWasiBrowserEntry,
|
|
37
|
+
WASI_ARTIFACT_METADATA_PREFIX,
|
|
38
|
+
} from './build.js'
|
|
17
39
|
|
|
18
40
|
const debug = debugFactory('artifacts')
|
|
19
41
|
|
|
42
|
+
interface WasiArtifactSource {
|
|
43
|
+
dir: string
|
|
44
|
+
files: Map<string, string>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface PendingWrite {
|
|
48
|
+
content: Buffer
|
|
49
|
+
source: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface WasiArtifactMetadata {
|
|
53
|
+
exports?: string[]
|
|
54
|
+
rootEntry: string | null
|
|
55
|
+
managedRootEntries: string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Removed configured targets are recognizable only when their output identity
|
|
59
|
+
// belongs to napi-rs's supported target set.
|
|
60
|
+
const supportedArtifactTargets = AVAILABLE_TARGETS.map(parseTriple)
|
|
61
|
+
|
|
20
62
|
export async function collectArtifacts(userOptions: ArtifactsOptions) {
|
|
21
63
|
const options = applyDefaultArtifactsOptions(userOptions)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
64
|
+
const resolvedPaths = resolvePackageReconciliationPaths(
|
|
65
|
+
options.cwd,
|
|
66
|
+
options.packageJsonPath,
|
|
67
|
+
[options.npmDir],
|
|
68
|
+
)
|
|
69
|
+
const { boundary } = resolvedPaths
|
|
70
|
+
return withFileSystemReconciliation(boundary, () =>
|
|
71
|
+
collectArtifactsUnlocked(options, resolvedPaths),
|
|
28
72
|
)
|
|
73
|
+
}
|
|
29
74
|
|
|
30
|
-
|
|
31
|
-
|
|
75
|
+
async function collectArtifactsUnlocked(
|
|
76
|
+
options: ReturnType<typeof applyDefaultArtifactsOptions>,
|
|
77
|
+
resolvedPaths: ReturnType<typeof resolvePackageReconciliationPaths>,
|
|
78
|
+
) {
|
|
79
|
+
const requestedCwd = resolve(options.cwd)
|
|
80
|
+
const cwd = resolvedPaths.cwd
|
|
81
|
+
const resolvePath = (...paths: string[]) => {
|
|
82
|
+
const requestedPath = resolve(requestedCwd, ...paths)
|
|
83
|
+
return isPathAtOrBelow(requestedPath, requestedCwd)
|
|
84
|
+
? resolve(cwd, relative(requestedCwd, requestedPath))
|
|
85
|
+
: requestedPath
|
|
86
|
+
}
|
|
87
|
+
const packageJsonPath = resolvedPaths.packageJsonPath
|
|
88
|
+
const packageRoot = resolvedPaths.packageRoot
|
|
89
|
+
const { targets, binaryName, packageName, packageJson } =
|
|
90
|
+
await readNapiConfig(
|
|
91
|
+
packageJsonPath,
|
|
92
|
+
options.configPath ? resolvePath(options.configPath) : undefined,
|
|
93
|
+
)
|
|
94
|
+
const npmDir = resolvedPaths.managedPaths[0]
|
|
95
|
+
const outputDir = await realpath(resolvePath(options.outputDir))
|
|
96
|
+
const buildOutputDir = options.buildOutputDir
|
|
97
|
+
? resolvePath(options.buildOutputDir)
|
|
98
|
+
: cwd
|
|
99
|
+
const managedTargetDirs = [
|
|
100
|
+
...new Set(
|
|
101
|
+
[...supportedArtifactTargets, ...targets].map(
|
|
102
|
+
(target) => target.platformArchABI,
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
].map((target) => join(npmDir, target))
|
|
106
|
+
const excludedSourceRoots = isStrictDescendant(outputDir, npmDir)
|
|
107
|
+
? [npmDir]
|
|
108
|
+
: resolve(outputDir) === resolve(npmDir)
|
|
109
|
+
? managedTargetDirs
|
|
110
|
+
: []
|
|
111
|
+
const protectedSourcePaths = new Set(
|
|
112
|
+
await collectRegularFiles(outputDir, excludedSourceRoots),
|
|
32
113
|
)
|
|
33
114
|
|
|
34
115
|
const universalSourceBins = new Set(
|
|
35
116
|
targets
|
|
36
|
-
.filter((
|
|
37
|
-
.flatMap((
|
|
38
|
-
UniArchsByPlatform[
|
|
117
|
+
.filter((target) => target.arch === 'universal')
|
|
118
|
+
.flatMap((target) =>
|
|
119
|
+
UniArchsByPlatform[target.platform]?.map(
|
|
120
|
+
(arch) => `${target.platform}-${arch}`,
|
|
121
|
+
),
|
|
39
122
|
)
|
|
40
123
|
.filter(Boolean) as string[],
|
|
41
124
|
)
|
|
42
125
|
|
|
43
|
-
await collectNodeBinaries(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
126
|
+
const artifacts = await collectNodeBinaries(outputDir, excludedSourceRoots)
|
|
127
|
+
const artifactsByIdentity = collectArtifactIdentities(artifacts, binaryName)
|
|
128
|
+
rejectDuplicateArtifactIdentities(artifactsByIdentity)
|
|
129
|
+
|
|
130
|
+
const expectedArtifacts = new Map<string, Target>()
|
|
131
|
+
for (const target of targets) {
|
|
132
|
+
expectedArtifacts.set(artifactName(binaryName, target), target)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const unexpectedArtifacts = [...artifactsByIdentity].filter(
|
|
136
|
+
([identity]) =>
|
|
137
|
+
!expectedArtifacts.has(identity) &&
|
|
138
|
+
!universalSourceBins.has(artifactPlatformArchABI(identity)),
|
|
139
|
+
)
|
|
140
|
+
if (unexpectedArtifacts.length > 0) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Artifacts were found for unconfigured targets: ${unexpectedArtifacts
|
|
143
|
+
.map(([identity, paths]) => `${identity}: ${paths.join(', ')}`)
|
|
144
|
+
.sort()
|
|
145
|
+
.join('; ')}`,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const missingTargets = targets.filter(
|
|
150
|
+
(target) => !artifactsByIdentity.has(artifactName(binaryName, target)),
|
|
151
|
+
)
|
|
152
|
+
if (missingTargets.length > 0) {
|
|
153
|
+
await removeTargetDestinations(
|
|
154
|
+
resolvedPaths.boundary,
|
|
155
|
+
packageRoot,
|
|
156
|
+
npmDir,
|
|
157
|
+
binaryName,
|
|
158
|
+
missingTargets,
|
|
159
|
+
protectedSourcePaths,
|
|
160
|
+
)
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Missing artifacts for configured targets: ${missingTargets
|
|
163
|
+
.map(
|
|
164
|
+
(target) => `${target.triple} (${artifactName(binaryName, target)})`,
|
|
165
|
+
)
|
|
166
|
+
.join(', ')}`,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const wasiTargets = targets.filter((target) => target.platform === 'wasi')
|
|
171
|
+
const wasiSources = new Map<string, WasiArtifactSource>()
|
|
172
|
+
for (const target of wasiTargets) {
|
|
173
|
+
const artifactPath = artifactsByIdentity.get(
|
|
174
|
+
artifactName(binaryName, target),
|
|
175
|
+
)![0]
|
|
176
|
+
const candidates = options.buildOutputDir
|
|
177
|
+
? [buildOutputDir]
|
|
178
|
+
: [...new Set([dirname(artifactPath), cwd])]
|
|
179
|
+
try {
|
|
180
|
+
wasiSources.set(
|
|
181
|
+
target.platformArchABI,
|
|
182
|
+
await findWasiArtifactSource(candidates, binaryName, target),
|
|
183
|
+
)
|
|
184
|
+
} catch (error) {
|
|
185
|
+
await removeTargetDestinations(
|
|
186
|
+
resolvedPaths.boundary,
|
|
187
|
+
packageRoot,
|
|
188
|
+
npmDir,
|
|
189
|
+
binaryName,
|
|
190
|
+
[target],
|
|
191
|
+
protectedSourcePaths,
|
|
192
|
+
)
|
|
193
|
+
throw error
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const pendingWrites = new Map<string, PendingWrite>()
|
|
198
|
+
for (const target of targets) {
|
|
199
|
+
const identity = artifactName(binaryName, target)
|
|
200
|
+
const source = artifactsByIdentity.get(identity)![0]
|
|
201
|
+
const content = await readFileAsync(source)
|
|
202
|
+
addPendingWrite(
|
|
203
|
+
pendingWrites,
|
|
204
|
+
join(npmDir, target.platformArchABI, identity),
|
|
205
|
+
source,
|
|
206
|
+
content,
|
|
207
|
+
)
|
|
208
|
+
addPendingWrite(pendingWrites, join(packageRoot, identity), source, content)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const browserTarget =
|
|
212
|
+
wasiTargets.find((target) => !wasiTargetHasThreads(target)) ??
|
|
213
|
+
wasiTargets[0]
|
|
214
|
+
if (browserTarget) {
|
|
215
|
+
try {
|
|
216
|
+
await addWasiBrowserEntry(
|
|
217
|
+
pendingWrites,
|
|
218
|
+
packageRoot,
|
|
219
|
+
packageName,
|
|
220
|
+
binaryName,
|
|
221
|
+
browserTarget,
|
|
222
|
+
wasiSources.get(browserTarget.platformArchABI)!,
|
|
223
|
+
)
|
|
224
|
+
} catch (error) {
|
|
225
|
+
await removeTargetDestinations(
|
|
226
|
+
resolvedPaths.boundary,
|
|
227
|
+
packageRoot,
|
|
228
|
+
npmDir,
|
|
229
|
+
binaryName,
|
|
230
|
+
[browserTarget],
|
|
231
|
+
protectedSourcePaths,
|
|
232
|
+
)
|
|
233
|
+
throw error
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
await addArtifactRootEntry({
|
|
237
|
+
pendingWrites,
|
|
238
|
+
packageRoot,
|
|
239
|
+
packageMain: packageJson.main,
|
|
240
|
+
binaryName,
|
|
241
|
+
targets,
|
|
242
|
+
artifactsByIdentity,
|
|
243
|
+
wasiTargets,
|
|
244
|
+
wasiSources,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
for (const target of wasiTargets) {
|
|
248
|
+
const source = wasiSources.get(target.platformArchABI)!
|
|
249
|
+
const loaderSuffix = wasiLoaderSuffix(target.platformArchABI)
|
|
250
|
+
const wasiDir = join(npmDir, target.platformArchABI)
|
|
251
|
+
for (const fileName of requiredWasiFiles(binaryName, target)) {
|
|
252
|
+
const sourcePath = source.files.get(fileName)!
|
|
253
|
+
let content = await readFileAsync(sourcePath)
|
|
254
|
+
if (fileName === `${binaryName}.${loaderSuffix}-browser.js`) {
|
|
255
|
+
content = Buffer.from(
|
|
256
|
+
content
|
|
257
|
+
.toString('utf8')
|
|
258
|
+
.replace(
|
|
259
|
+
`new URL('./wasi-worker-browser.mjs', import.meta.url)`,
|
|
260
|
+
`new URL('${packageName}-${target.platformArchABI}/wasi-worker-browser.mjs', import.meta.url)`,
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
addPendingWrite(
|
|
265
|
+
pendingWrites,
|
|
266
|
+
join(wasiDir, fileName),
|
|
267
|
+
sourcePath,
|
|
268
|
+
content,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const staleManagedDestinations = await collectStaleManagedDestinations(
|
|
274
|
+
packageRoot,
|
|
275
|
+
npmDir,
|
|
276
|
+
binaryName,
|
|
277
|
+
targets,
|
|
278
|
+
pendingWrites,
|
|
279
|
+
await collectManagedRootEntries(packageRoot, binaryName, wasiSources),
|
|
280
|
+
protectedSourcePaths,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
await commitArtifactReconciliation(
|
|
284
|
+
resolvedPaths.boundary,
|
|
285
|
+
pendingWrites,
|
|
286
|
+
staleManagedDestinations,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function commitArtifactReconciliation(
|
|
291
|
+
reconciliationRoot: string,
|
|
292
|
+
pendingWrites: Map<string, PendingWrite>,
|
|
293
|
+
staleManagedDestinations: string[],
|
|
294
|
+
) {
|
|
295
|
+
const stagingRoot = await mkdtemp(join(tmpdir(), 'napi-rs-artifacts-stage-'))
|
|
296
|
+
try {
|
|
297
|
+
const writes = await Promise.all(
|
|
298
|
+
[...pendingWrites].map(
|
|
299
|
+
async ([destination, { content, source }], index) => {
|
|
300
|
+
const stagedSource = join(stagingRoot, String(index))
|
|
301
|
+
await writeFile(stagedSource, content)
|
|
80
302
|
debug.info(
|
|
81
|
-
`Write
|
|
303
|
+
`Write [${colors.yellowBright(source)}] to [${colors.yellowBright(
|
|
304
|
+
destination,
|
|
305
|
+
)}]`,
|
|
82
306
|
)
|
|
83
|
-
|
|
84
|
-
}
|
|
307
|
+
return { destination, source: stagedSource }
|
|
308
|
+
},
|
|
85
309
|
),
|
|
310
|
+
)
|
|
311
|
+
if (writes.length === 0 && staleManagedDestinations.length === 0) {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
await commitFileSystemTransaction(
|
|
315
|
+
reconciliationRoot,
|
|
316
|
+
writes,
|
|
317
|
+
staleManagedDestinations,
|
|
318
|
+
)
|
|
319
|
+
} finally {
|
|
320
|
+
await rm(stagingRoot, { force: true, recursive: true })
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function artifactName(binaryName: string, target: Target) {
|
|
325
|
+
const extension =
|
|
326
|
+
target.platform === 'wasi' || target.platform === 'wasm' ? 'wasm' : 'node'
|
|
327
|
+
return `${binaryName}.${target.platformArchABI}.${extension}`
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function artifactPlatformArchABI(identity: string) {
|
|
331
|
+
const parsed = parse(identity)
|
|
332
|
+
return parsed.name.split('.').pop()!
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function requiredWasiFiles(binaryName: string, target: Target) {
|
|
336
|
+
const loaderSuffix = wasiLoaderSuffix(target.platformArchABI)
|
|
337
|
+
const files = [
|
|
338
|
+
`${binaryName}.${loaderSuffix}.cjs`,
|
|
339
|
+
`${binaryName}.${loaderSuffix}.d.cts`,
|
|
340
|
+
`${binaryName}.${loaderSuffix}-browser.js`,
|
|
341
|
+
]
|
|
342
|
+
if (wasiTargetHasThreads(target)) {
|
|
343
|
+
files.push('wasi-worker.mjs', 'wasi-worker-browser.mjs')
|
|
344
|
+
} else {
|
|
345
|
+
files.push(
|
|
346
|
+
`${binaryName}.${loaderSuffix}-deferred.js`,
|
|
347
|
+
`${binaryName}.${loaderSuffix}-deferred.d.ts`,
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
return files
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function findWasiArtifactSource(
|
|
354
|
+
candidateDirs: string[],
|
|
355
|
+
binaryName: string,
|
|
356
|
+
target: Target,
|
|
357
|
+
): Promise<WasiArtifactSource> {
|
|
358
|
+
const requiredFiles = requiredWasiFiles(binaryName, target)
|
|
359
|
+
const failures: string[] = []
|
|
360
|
+
for (const dir of candidateDirs) {
|
|
361
|
+
const files = new Map<string, string>()
|
|
362
|
+
const missing: string[] = []
|
|
363
|
+
for (const fileName of requiredFiles) {
|
|
364
|
+
const path = join(dir, fileName)
|
|
365
|
+
if (await fileExists(path)) {
|
|
366
|
+
files.set(fileName, path)
|
|
367
|
+
} else {
|
|
368
|
+
missing.push(fileName)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const browserPath = join(dir, 'browser.js')
|
|
372
|
+
if (await fileExists(browserPath)) {
|
|
373
|
+
files.set('browser.js', browserPath)
|
|
374
|
+
}
|
|
375
|
+
if (missing.length === 0) {
|
|
376
|
+
return { dir, files }
|
|
377
|
+
}
|
|
378
|
+
const failure = `${dir} (missing ${missing.join(', ')})`
|
|
379
|
+
// A candidate containing part of a loader set is authoritative but
|
|
380
|
+
// incomplete. Falling through would combine it with stale local outputs.
|
|
381
|
+
if (files.size > 0) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Incomplete artifact source found for ${target.triple}: ${failure}`,
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
failures.push(failure)
|
|
387
|
+
}
|
|
388
|
+
throw new Error(
|
|
389
|
+
`No complete artifact source found for ${target.triple}: ${failures.join(
|
|
390
|
+
'; ',
|
|
391
|
+
)}`,
|
|
86
392
|
)
|
|
393
|
+
}
|
|
87
394
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)}] to [${colors.yellowBright(wasiDir)}]`,
|
|
115
|
-
)
|
|
116
|
-
await writeFileAsync(
|
|
117
|
-
join(wasiDir, `wasi-worker.mjs`),
|
|
118
|
-
await readFileAsync(workerFile),
|
|
119
|
-
)
|
|
120
|
-
debug.info(
|
|
121
|
-
`Move wasi browser entry file [${colors.yellowBright(
|
|
122
|
-
browserEntry,
|
|
123
|
-
)}] to [${colors.yellowBright(wasiDir)}]`,
|
|
124
|
-
)
|
|
125
|
-
await writeFileAsync(
|
|
126
|
-
join(wasiDir, `${binaryName}.wasi-browser.js`),
|
|
127
|
-
// https://github.com/vitejs/vite/issues/8427
|
|
128
|
-
(await readFileAsync(browserEntry, 'utf8')).replace(
|
|
129
|
-
`new URL('./wasi-worker-browser.mjs', import.meta.url)`,
|
|
130
|
-
`new URL('${packageName}-wasm32-wasi/wasi-worker-browser.mjs', import.meta.url)`,
|
|
395
|
+
async function addWasiBrowserEntry(
|
|
396
|
+
pendingWrites: Map<string, PendingWrite>,
|
|
397
|
+
packageRoot: string,
|
|
398
|
+
packageName: string,
|
|
399
|
+
binaryName: string,
|
|
400
|
+
target: Target,
|
|
401
|
+
source: WasiArtifactSource,
|
|
402
|
+
) {
|
|
403
|
+
const bindingSource = source.files.get(
|
|
404
|
+
`${binaryName}.${wasiLoaderSuffix(target.platformArchABI)}.cjs`,
|
|
405
|
+
)!
|
|
406
|
+
const metadata = parseWasiArtifactMetadata(
|
|
407
|
+
await readFileAsync(bindingSource, 'utf8'),
|
|
408
|
+
bindingSource,
|
|
409
|
+
)
|
|
410
|
+
if (metadata?.exports !== undefined) {
|
|
411
|
+
addPendingWrite(
|
|
412
|
+
pendingWrites,
|
|
413
|
+
join(packageRoot, 'browser.js'),
|
|
414
|
+
bindingSource,
|
|
415
|
+
Buffer.from(
|
|
416
|
+
createWasiBrowserEntry(
|
|
417
|
+
packageName,
|
|
418
|
+
target.platformArchABI,
|
|
419
|
+
metadata.exports,
|
|
420
|
+
),
|
|
131
421
|
),
|
|
132
422
|
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const browserSource = source.files.get('browser.js')
|
|
427
|
+
if (!browserSource) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`WASI artifact source ${source.dir} is incomplete: missing required root entry browser.js`,
|
|
137
430
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
431
|
+
}
|
|
432
|
+
addPendingWrite(
|
|
433
|
+
pendingWrites,
|
|
434
|
+
join(packageRoot, 'browser.js'),
|
|
435
|
+
browserSource,
|
|
436
|
+
await readFileAsync(browserSource),
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function addWasiRootEntry(
|
|
441
|
+
pendingWrites: Map<string, PendingWrite>,
|
|
442
|
+
packageRoot: string,
|
|
443
|
+
binaryName: string,
|
|
444
|
+
rootTarget: Target,
|
|
445
|
+
source: WasiArtifactSource,
|
|
446
|
+
packageMain: string | undefined,
|
|
447
|
+
) {
|
|
448
|
+
const bindingSource = source.files.get(
|
|
449
|
+
`${binaryName}.${wasiLoaderSuffix(rootTarget.platformArchABI)}.cjs`,
|
|
450
|
+
)!
|
|
451
|
+
const metadata = parseWasiArtifactMetadata(
|
|
452
|
+
await readFileAsync(bindingSource, 'utf8'),
|
|
453
|
+
bindingSource,
|
|
454
|
+
)
|
|
455
|
+
const rootEntry =
|
|
456
|
+
metadata === undefined
|
|
457
|
+
? await findLegacyRootEntry(source.dir, packageMain)
|
|
458
|
+
: metadata.rootEntry
|
|
459
|
+
if (rootEntry === null) {
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const sourcePath = resolveArtifactRelativePath(
|
|
464
|
+
source.dir,
|
|
465
|
+
rootEntry,
|
|
466
|
+
'WASI root entry',
|
|
467
|
+
)
|
|
468
|
+
if (!(await fileExists(sourcePath.absolute))) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`WASI artifact source ${source.dir} is incomplete: missing required root entry ${rootEntry}`,
|
|
141
471
|
)
|
|
142
472
|
}
|
|
473
|
+
const destination = resolveArtifactRelativePath(
|
|
474
|
+
packageRoot,
|
|
475
|
+
sourcePath.relative,
|
|
476
|
+
'WASI root entry destination',
|
|
477
|
+
).absolute
|
|
478
|
+
addPendingWrite(
|
|
479
|
+
pendingWrites,
|
|
480
|
+
destination,
|
|
481
|
+
sourcePath.absolute,
|
|
482
|
+
await readFileAsync(sourcePath.absolute),
|
|
483
|
+
)
|
|
143
484
|
}
|
|
144
485
|
|
|
145
|
-
async function
|
|
486
|
+
async function addArtifactRootEntry({
|
|
487
|
+
pendingWrites,
|
|
488
|
+
packageRoot,
|
|
489
|
+
packageMain,
|
|
490
|
+
binaryName,
|
|
491
|
+
targets,
|
|
492
|
+
artifactsByIdentity,
|
|
493
|
+
wasiTargets,
|
|
494
|
+
wasiSources,
|
|
495
|
+
}: {
|
|
496
|
+
pendingWrites: Map<string, PendingWrite>
|
|
497
|
+
packageRoot: string
|
|
498
|
+
packageMain: string | undefined
|
|
499
|
+
binaryName: string
|
|
500
|
+
targets: Target[]
|
|
501
|
+
artifactsByIdentity: Map<string, string[]>
|
|
502
|
+
wasiTargets: Target[]
|
|
503
|
+
wasiSources: Map<string, WasiArtifactSource>
|
|
504
|
+
}) {
|
|
505
|
+
const nativeTargets = targets.filter((target) => target.platform !== 'wasi')
|
|
506
|
+
const rootCandidates = packageRootEntryCandidates(packageMain)
|
|
507
|
+
|
|
508
|
+
for (const target of nativeTargets) {
|
|
509
|
+
const artifactPath = artifactsByIdentity.get(
|
|
510
|
+
artifactName(binaryName, target),
|
|
511
|
+
)?.[0]
|
|
512
|
+
if (!artifactPath) {
|
|
513
|
+
continue
|
|
514
|
+
}
|
|
515
|
+
const sourceDir = dirname(artifactPath)
|
|
516
|
+
for (const candidate of rootCandidates) {
|
|
517
|
+
const source = resolveArtifactRelativePath(
|
|
518
|
+
sourceDir,
|
|
519
|
+
candidate,
|
|
520
|
+
'native root entry',
|
|
521
|
+
)
|
|
522
|
+
if (!(await fileExists(source.absolute))) {
|
|
523
|
+
continue
|
|
524
|
+
}
|
|
525
|
+
const destination = resolveArtifactRelativePath(
|
|
526
|
+
packageRoot,
|
|
527
|
+
source.relative,
|
|
528
|
+
'native root entry destination',
|
|
529
|
+
)
|
|
530
|
+
addPendingWrite(
|
|
531
|
+
pendingWrites,
|
|
532
|
+
destination.absolute,
|
|
533
|
+
source.absolute,
|
|
534
|
+
await readFileAsync(source.absolute),
|
|
535
|
+
)
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (nativeTargets.length > 0) {
|
|
541
|
+
for (const candidate of rootCandidates) {
|
|
542
|
+
const existing = resolveArtifactRelativePath(
|
|
543
|
+
packageRoot,
|
|
544
|
+
candidate,
|
|
545
|
+
'existing native root entry',
|
|
546
|
+
)
|
|
547
|
+
if (!(await fileExists(existing.absolute))) {
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
addPendingWrite(
|
|
551
|
+
pendingWrites,
|
|
552
|
+
existing.absolute,
|
|
553
|
+
existing.absolute,
|
|
554
|
+
await readFileAsync(existing.absolute),
|
|
555
|
+
)
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const rootTarget =
|
|
561
|
+
wasiTargets.find((target) => wasiTargetHasThreads(target)) ?? wasiTargets[0]
|
|
562
|
+
if (rootTarget) {
|
|
563
|
+
await addWasiRootEntry(
|
|
564
|
+
pendingWrites,
|
|
565
|
+
packageRoot,
|
|
566
|
+
binaryName,
|
|
567
|
+
rootTarget,
|
|
568
|
+
wasiSources.get(rootTarget.platformArchABI)!,
|
|
569
|
+
packageMain,
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function packageRootEntryCandidates(packageMain: string | undefined) {
|
|
575
|
+
return [
|
|
576
|
+
...(packageMain && /\.[cm]?js$/i.test(packageMain) ? [packageMain] : []),
|
|
577
|
+
'index.js',
|
|
578
|
+
]
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function parseWasiArtifactMetadata(content: string, source: string) {
|
|
582
|
+
const firstLine = content.split(/\r?\n/, 1)[0]
|
|
583
|
+
if (!firstLine.startsWith(WASI_ARTIFACT_METADATA_PREFIX)) {
|
|
584
|
+
return undefined
|
|
585
|
+
}
|
|
586
|
+
let metadata: unknown
|
|
587
|
+
try {
|
|
588
|
+
metadata = JSON.parse(firstLine.slice(WASI_ARTIFACT_METADATA_PREFIX.length))
|
|
589
|
+
} catch (error) {
|
|
590
|
+
throw new Error(`Invalid WASI artifact metadata in ${source}`, {
|
|
591
|
+
cause: error,
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
if (typeof metadata !== 'object' || metadata === null) {
|
|
595
|
+
throw new Error(`Unsupported WASI artifact metadata in ${source}`)
|
|
596
|
+
}
|
|
597
|
+
const record = metadata as Record<string, unknown>
|
|
598
|
+
if (
|
|
599
|
+
(record.version !== 1 && record.version !== 2) ||
|
|
600
|
+
(record.rootEntry !== null && typeof record.rootEntry !== 'string')
|
|
601
|
+
) {
|
|
602
|
+
throw new Error(`Unsupported WASI artifact metadata in ${source}`)
|
|
603
|
+
}
|
|
604
|
+
const managedRootEntries =
|
|
605
|
+
record.version === 1
|
|
606
|
+
? ['browser.js', ...(record.rootEntry ? [record.rootEntry] : [])]
|
|
607
|
+
: record.managedRootEntries
|
|
608
|
+
if (
|
|
609
|
+
!Array.isArray(managedRootEntries) ||
|
|
610
|
+
!managedRootEntries.every((entry) => typeof entry === 'string')
|
|
611
|
+
) {
|
|
612
|
+
throw new Error(`Unsupported WASI artifact metadata in ${source}`)
|
|
613
|
+
}
|
|
614
|
+
const exports = record.exports
|
|
615
|
+
if (
|
|
616
|
+
exports !== undefined &&
|
|
617
|
+
(!Array.isArray(exports) ||
|
|
618
|
+
!exports.every((entry) => typeof entry === 'string'))
|
|
619
|
+
) {
|
|
620
|
+
throw new Error(`Unsupported WASI artifact metadata in ${source}`)
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
exports: exports as string[] | undefined,
|
|
624
|
+
rootEntry: record.rootEntry as string | null,
|
|
625
|
+
managedRootEntries: [...new Set(managedRootEntries)],
|
|
626
|
+
} satisfies WasiArtifactMetadata
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function findLegacyRootEntry(
|
|
630
|
+
sourceDir: string,
|
|
631
|
+
packageMain: string | undefined,
|
|
632
|
+
) {
|
|
633
|
+
const candidates = packageRootEntryCandidates(packageMain)
|
|
634
|
+
for (const candidate of new Set(candidates)) {
|
|
635
|
+
const path = resolveArtifactRelativePath(
|
|
636
|
+
sourceDir,
|
|
637
|
+
candidate,
|
|
638
|
+
'legacy WASI root entry',
|
|
639
|
+
)
|
|
640
|
+
if (await fileExists(path.absolute)) {
|
|
641
|
+
return path.relative
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
throw new Error(
|
|
645
|
+
`WASI artifact source ${sourceDir} is incomplete: missing required root entry ${candidates.join(
|
|
646
|
+
' or ',
|
|
647
|
+
)}`,
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function resolveArtifactRelativePath(
|
|
652
|
+
root: string,
|
|
653
|
+
entry: string,
|
|
654
|
+
description: string,
|
|
655
|
+
) {
|
|
656
|
+
if (!entry || isAbsolute(entry)) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
`${description} must be a non-empty relative path: ${entry}`,
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
const absolute = resolve(root, entry)
|
|
662
|
+
const relativePath = relative(root, absolute)
|
|
663
|
+
if (
|
|
664
|
+
relativePath === '' ||
|
|
665
|
+
relativePath === '..' ||
|
|
666
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
667
|
+
isAbsolute(relativePath)
|
|
668
|
+
) {
|
|
669
|
+
throw new Error(`${description} escapes its output directory: ${entry}`)
|
|
670
|
+
}
|
|
671
|
+
return { absolute, relative: relativePath }
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function addPendingWrite(
|
|
675
|
+
writes: Map<string, PendingWrite>,
|
|
676
|
+
destination: string,
|
|
677
|
+
source: string,
|
|
678
|
+
content: Buffer,
|
|
679
|
+
) {
|
|
680
|
+
const existing = writes.get(destination)
|
|
681
|
+
if (existing) {
|
|
682
|
+
if (!existing.content.equals(content)) {
|
|
683
|
+
throw new Error(
|
|
684
|
+
`Conflicting artifacts target ${destination}: ${existing.source} and ${source}`,
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
return
|
|
688
|
+
}
|
|
689
|
+
writes.set(destination, { content, source })
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function removeTargetDestinations(
|
|
693
|
+
reconciliationRoot: string,
|
|
694
|
+
packageRoot: string,
|
|
695
|
+
npmDir: string,
|
|
696
|
+
binaryName: string,
|
|
697
|
+
targets: Target[],
|
|
698
|
+
protectedSourcePaths: Set<string>,
|
|
699
|
+
) {
|
|
700
|
+
const paths = targets.flatMap((target) =>
|
|
701
|
+
targetDestinationPaths(packageRoot, npmDir, binaryName, target),
|
|
702
|
+
)
|
|
703
|
+
const removals = [
|
|
704
|
+
...new Set(
|
|
705
|
+
paths.filter((path) => !protectedSourcePaths.has(resolve(path))),
|
|
706
|
+
),
|
|
707
|
+
]
|
|
708
|
+
if (removals.length > 0) {
|
|
709
|
+
await commitFileSystemTransaction(reconciliationRoot, [], removals)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function targetDestinationPaths(
|
|
714
|
+
packageRoot: string,
|
|
715
|
+
npmDir: string,
|
|
716
|
+
binaryName: string,
|
|
717
|
+
target: Target,
|
|
718
|
+
) {
|
|
719
|
+
const identity = artifactName(binaryName, target)
|
|
720
|
+
const paths = [
|
|
721
|
+
join(packageRoot, identity),
|
|
722
|
+
join(npmDir, target.platformArchABI, identity),
|
|
723
|
+
]
|
|
724
|
+
if (target.platform === 'wasi') {
|
|
725
|
+
const wasiDir = join(npmDir, target.platformArchABI)
|
|
726
|
+
for (const fileName of allManagedWasiFiles(binaryName, target)) {
|
|
727
|
+
paths.push(join(wasiDir, fileName))
|
|
728
|
+
}
|
|
729
|
+
for (const fileName of requiredWasiFiles(binaryName, target)) {
|
|
730
|
+
paths.push(join(packageRoot, fileName))
|
|
731
|
+
}
|
|
732
|
+
paths.push(join(packageRoot, `${binaryName}.wasm`))
|
|
733
|
+
}
|
|
734
|
+
return paths
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async function collectStaleManagedDestinations(
|
|
738
|
+
packageRoot: string,
|
|
739
|
+
npmDir: string,
|
|
740
|
+
binaryName: string,
|
|
741
|
+
targets: Target[],
|
|
742
|
+
pendingWrites: Map<string, PendingWrite>,
|
|
743
|
+
managedRootEntries: string[],
|
|
744
|
+
protectedSourcePaths: Set<string>,
|
|
745
|
+
) {
|
|
746
|
+
const stalePaths: string[] = []
|
|
747
|
+
const managedBinaryFiles = new Set(
|
|
748
|
+
[...supportedArtifactTargets, ...targets].map((target) =>
|
|
749
|
+
artifactName(binaryName, target),
|
|
750
|
+
),
|
|
751
|
+
)
|
|
752
|
+
const managedWasiFiles = new Set<string>()
|
|
753
|
+
for (const loaderSuffix of new Set([
|
|
754
|
+
'wasi',
|
|
755
|
+
'wasip1',
|
|
756
|
+
...targets
|
|
757
|
+
.filter((target) => target.platform === 'wasi')
|
|
758
|
+
.map((target) => wasiLoaderSuffix(target.platformArchABI)),
|
|
759
|
+
])) {
|
|
760
|
+
for (const fileName of allManagedWasiFilesForSuffix(
|
|
761
|
+
binaryName,
|
|
762
|
+
loaderSuffix,
|
|
763
|
+
)) {
|
|
764
|
+
managedWasiFiles.add(fileName)
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
for (const target of targets) {
|
|
769
|
+
const targetDir = join(npmDir, target.platformArchABI)
|
|
770
|
+
if (!(await fileExists(targetDir))) {
|
|
771
|
+
continue
|
|
772
|
+
}
|
|
773
|
+
for (const entry of await readdirAsync(targetDir, {
|
|
774
|
+
withFileTypes: true,
|
|
775
|
+
})) {
|
|
776
|
+
const path = join(targetDir, entry.name)
|
|
777
|
+
if (
|
|
778
|
+
entry.isFile() &&
|
|
779
|
+
(managedBinaryFiles.has(entry.name) ||
|
|
780
|
+
(target.platform === 'wasi' && managedWasiFiles.has(entry.name))) &&
|
|
781
|
+
!pendingWrites.has(path)
|
|
782
|
+
) {
|
|
783
|
+
stalePaths.push(path)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (await fileExists(packageRoot)) {
|
|
789
|
+
for (const entry of await readdirAsync(packageRoot, {
|
|
790
|
+
withFileTypes: true,
|
|
791
|
+
})) {
|
|
792
|
+
const path = join(packageRoot, entry.name)
|
|
793
|
+
if (
|
|
794
|
+
entry.isFile() &&
|
|
795
|
+
managedBinaryFiles.has(entry.name) &&
|
|
796
|
+
!pendingWrites.has(path)
|
|
797
|
+
) {
|
|
798
|
+
stalePaths.push(path)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
for (const entry of managedRootEntries) {
|
|
804
|
+
const path = resolveArtifactRelativePath(
|
|
805
|
+
packageRoot,
|
|
806
|
+
entry,
|
|
807
|
+
'managed WASI root entry',
|
|
808
|
+
).absolute
|
|
809
|
+
if (!pendingWrites.has(path)) {
|
|
810
|
+
stalePaths.push(path)
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return [
|
|
815
|
+
...new Set(
|
|
816
|
+
stalePaths.filter((path) => !protectedSourcePaths.has(resolve(path))),
|
|
817
|
+
),
|
|
818
|
+
]
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function allManagedWasiFiles(binaryName: string, target: Target) {
|
|
822
|
+
return allManagedWasiFilesForSuffix(
|
|
823
|
+
binaryName,
|
|
824
|
+
wasiLoaderSuffix(target.platformArchABI),
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function allManagedWasiFilesForSuffix(
|
|
829
|
+
binaryName: string,
|
|
830
|
+
loaderSuffix: string,
|
|
831
|
+
) {
|
|
832
|
+
return [
|
|
833
|
+
`${binaryName}.${loaderSuffix}.cjs`,
|
|
834
|
+
`${binaryName}.${loaderSuffix}.d.cts`,
|
|
835
|
+
`${binaryName}.${loaderSuffix}-browser.js`,
|
|
836
|
+
`${binaryName}.${loaderSuffix}-deferred.js`,
|
|
837
|
+
`${binaryName}.${loaderSuffix}-deferred.d.ts`,
|
|
838
|
+
'wasi-worker.mjs',
|
|
839
|
+
'wasi-worker-browser.mjs',
|
|
840
|
+
]
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function collectManagedRootEntries(
|
|
844
|
+
packageRoot: string,
|
|
845
|
+
binaryName: string,
|
|
846
|
+
wasiSources: Map<string, WasiArtifactSource>,
|
|
847
|
+
) {
|
|
848
|
+
const entries = new Set<string>()
|
|
849
|
+
const loaderPaths = new Set<string>()
|
|
850
|
+
for (const loaderSuffix of ['wasi', 'wasip1']) {
|
|
851
|
+
loaderPaths.add(join(packageRoot, `${binaryName}.${loaderSuffix}.cjs`))
|
|
852
|
+
}
|
|
853
|
+
for (const source of wasiSources.values()) {
|
|
854
|
+
for (const [fileName, path] of source.files) {
|
|
855
|
+
if (fileName.endsWith('.cjs')) {
|
|
856
|
+
loaderPaths.add(path)
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
for (const path of loaderPaths) {
|
|
862
|
+
if (!(await fileExists(path))) {
|
|
863
|
+
continue
|
|
864
|
+
}
|
|
865
|
+
const metadata = parseWasiArtifactMetadata(
|
|
866
|
+
await readFileAsync(path, 'utf8'),
|
|
867
|
+
path,
|
|
868
|
+
)
|
|
869
|
+
for (const entry of metadata?.managedRootEntries ?? []) {
|
|
870
|
+
entries.add(entry)
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return [...entries]
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function collectArtifactIdentities(filePaths: string[], binaryName: string) {
|
|
877
|
+
const artifactsByIdentity = new Map<string, string[]>()
|
|
878
|
+
for (const filePath of filePaths) {
|
|
879
|
+
if (filePath.endsWith('.debug.wasm')) {
|
|
880
|
+
continue
|
|
881
|
+
}
|
|
882
|
+
const parsedName = parse(filePath)
|
|
883
|
+
const terms = parsedName.name.split('.')
|
|
884
|
+
terms.pop()
|
|
885
|
+
if (terms.join('.') !== binaryName) {
|
|
886
|
+
continue
|
|
887
|
+
}
|
|
888
|
+
const matchingArtifacts = artifactsByIdentity.get(parsedName.base)
|
|
889
|
+
if (matchingArtifacts) {
|
|
890
|
+
matchingArtifacts.push(filePath)
|
|
891
|
+
} else {
|
|
892
|
+
artifactsByIdentity.set(parsedName.base, [filePath])
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return artifactsByIdentity
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function rejectDuplicateArtifactIdentities(
|
|
899
|
+
artifactsByIdentity: Map<string, string[]>,
|
|
900
|
+
) {
|
|
901
|
+
const duplicates = [...artifactsByIdentity]
|
|
902
|
+
.filter(([, filePaths]) => filePaths.length > 1)
|
|
903
|
+
.map(
|
|
904
|
+
([identity, filePaths]) =>
|
|
905
|
+
`${identity}: ${[...filePaths].sort().join(', ')}`,
|
|
906
|
+
)
|
|
907
|
+
.sort()
|
|
908
|
+
if (duplicates.length > 0) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`Multiple artifacts found for binary artifact identities: ${duplicates.join('; ')}. Ensure merged CI artifacts contain exactly one build for each configured target or narrow --output-dir.`,
|
|
911
|
+
)
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async function collectNodeBinaries(root: string, excludedRoots: string[] = []) {
|
|
916
|
+
const resolvedRoot = resolve(root)
|
|
917
|
+
if (
|
|
918
|
+
excludedRoots.some((excludedRoot) =>
|
|
919
|
+
isPathAtOrBelow(resolvedRoot, excludedRoot),
|
|
920
|
+
)
|
|
921
|
+
) {
|
|
922
|
+
return []
|
|
923
|
+
}
|
|
146
924
|
const files = await readdirAsync(root, { withFileTypes: true })
|
|
147
925
|
const nodeBinaries = files
|
|
148
926
|
.filter(
|
|
@@ -155,8 +933,56 @@ async function collectNodeBinaries(root: string) {
|
|
|
155
933
|
const dirs = files.filter((file) => file.isDirectory())
|
|
156
934
|
for (const dir of dirs) {
|
|
157
935
|
if (dir.name !== 'node_modules') {
|
|
158
|
-
nodeBinaries.push(
|
|
936
|
+
nodeBinaries.push(
|
|
937
|
+
...(await collectNodeBinaries(join(root, dir.name), excludedRoots)),
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return nodeBinaries.sort()
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function collectRegularFiles(
|
|
945
|
+
root: string,
|
|
946
|
+
excludedRoots: string[] = [],
|
|
947
|
+
): Promise<string[]> {
|
|
948
|
+
const resolvedRoot = resolve(root)
|
|
949
|
+
if (
|
|
950
|
+
excludedRoots.some((excludedRoot) =>
|
|
951
|
+
isPathAtOrBelow(resolvedRoot, excludedRoot),
|
|
952
|
+
)
|
|
953
|
+
) {
|
|
954
|
+
return []
|
|
955
|
+
}
|
|
956
|
+
const files = await readdirAsync(resolvedRoot, { withFileTypes: true })
|
|
957
|
+
const regularFiles = files
|
|
958
|
+
.filter((entry) => entry.isFile())
|
|
959
|
+
.map((entry) => join(resolvedRoot, entry.name))
|
|
960
|
+
for (const entry of files) {
|
|
961
|
+
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
962
|
+
regularFiles.push(
|
|
963
|
+
...(await collectRegularFiles(
|
|
964
|
+
join(resolvedRoot, entry.name),
|
|
965
|
+
excludedRoots,
|
|
966
|
+
)),
|
|
967
|
+
)
|
|
159
968
|
}
|
|
160
969
|
}
|
|
161
|
-
return
|
|
970
|
+
return regularFiles
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function isStrictDescendant(root: string, candidate: string) {
|
|
974
|
+
const relativePath = relative(resolve(root), resolve(candidate))
|
|
975
|
+
return (
|
|
976
|
+
relativePath !== '' &&
|
|
977
|
+
!relativePath.startsWith('..') &&
|
|
978
|
+
!isAbsolute(relativePath)
|
|
979
|
+
)
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function isPathAtOrBelow(path: string, root: string) {
|
|
983
|
+
const relativePath = relative(resolve(root), resolve(path))
|
|
984
|
+
return (
|
|
985
|
+
relativePath === '' ||
|
|
986
|
+
(!relativePath.startsWith('..') && !isAbsolute(relativePath))
|
|
987
|
+
)
|
|
162
988
|
}
|