@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.
Files changed (43) hide show
  1. package/README.md +33 -0
  2. package/cli.mjs +9 -12
  3. package/dist/cli.js +9249 -1312
  4. package/dist/index.cjs +9269 -1332
  5. package/dist/index.d.cts +175 -157
  6. package/dist/index.d.ts +175 -157
  7. package/dist/index.js +9249 -1312
  8. package/docs/artifacts.md +33 -0
  9. package/docs/build.md +58 -0
  10. package/docs/create-npm-dirs.md +32 -0
  11. package/docs/new.md +39 -0
  12. package/docs/pre-publish.md +37 -0
  13. package/docs/rename.md +37 -0
  14. package/docs/universalize.md +31 -0
  15. package/docs/version.md +31 -0
  16. package/docs/wasi.md +161 -0
  17. package/package.json +11 -10
  18. package/src/api/__tests__/__snapshots__/templates.spec.ts.md +1019 -114
  19. package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
  20. package/src/api/__tests__/artifacts.spec.ts +17 -0
  21. package/src/api/__tests__/create-npm-dirs.spec.ts +60 -15
  22. package/src/api/__tests__/new.spec.ts +41 -5
  23. package/src/api/artifacts.ts +934 -108
  24. package/src/api/build.ts +1317 -188
  25. package/src/api/create-npm-dirs.ts +475 -102
  26. package/src/api/new.ts +354 -35
  27. package/src/api/pre-publish.ts +2845 -99
  28. package/src/api/rename.ts +1638 -124
  29. package/src/api/templates/js-binding.ts +216 -38
  30. package/src/api/templates/load-wasi-template.ts +1641 -111
  31. package/src/api/templates/wasi-worker-template.ts +32 -3
  32. package/src/commands/rename.ts +8 -3
  33. package/src/def/artifacts.ts +2 -2
  34. package/src/utils/__tests__/__snapshots__/target.spec.ts.md +2 -2
  35. package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
  36. package/src/utils/__tests__/misc.spec.ts +124 -3
  37. package/src/utils/__tests__/version.spec.ts +47 -1
  38. package/src/utils/config.ts +15 -2
  39. package/src/utils/metadata.ts +155 -47
  40. package/src/utils/misc.ts +6079 -0
  41. package/src/utils/target.ts +66 -8
  42. package/src/utils/typegen.ts +1223 -63
  43. package/src/utils/version.ts +96 -0
@@ -1,26 +1,47 @@
1
+ import {
2
+ lstat,
3
+ mkdtemp,
4
+ readFile,
5
+ readdir,
6
+ rm,
7
+ rmdir,
8
+ writeFile,
9
+ } from 'node:fs/promises'
1
10
  import { createRequire } from 'node:module'
2
- import { join, resolve } from 'node:path'
3
-
4
- import { Comparator, Range, minVersion, subset } from 'semver'
11
+ import { tmpdir } from 'node:os'
12
+ import { basename, isAbsolute, join, resolve } from 'node:path'
5
13
 
6
14
  const require = createRequire(import.meta.url)
7
- const minimumWasiNodeVersion = '>=14.0.0'
15
+ const directBufferDependency = '^6.0.3'
8
16
 
9
17
  import {
10
18
  applyDefaultCreateNpmDirsOptions,
11
19
  type CreateNpmDirsOptions,
12
20
  } from '../def/create-npm-dirs.js'
13
21
  import {
22
+ commitFileSystemTransaction,
23
+ createWasmModuleTypeDef,
14
24
  debugFactory,
25
+ MINIMUM_WASI_NODE_VERSION,
26
+ parseTriple,
15
27
  readNapiConfig,
16
- mkdirAsync as rawMkdirAsync,
17
28
  pick,
18
- writeFileAsync as rawWriteFileAsync,
29
+ resolvePackageReconciliationPaths,
30
+ restrictWasiNodeEngine,
31
+ wasiLoaderSuffix,
32
+ wasiTargetHasThreads,
33
+ withFileSystemReconciliation,
34
+ type FileSystemTransactionWrite,
19
35
  type Target,
20
36
  type CommonPackageJsonFields,
21
37
  } from '../utils/index.js'
22
38
 
23
39
  const debug = debugFactory('create-npm-dirs')
40
+ const MANAGED_WASI_PACKAGE_TARGETS = new Map([
41
+ ['wasm32-wasi', 'wasm32-wasip1-threads'],
42
+ ['wasm32-wasip1', 'wasm32-wasip1'],
43
+ ])
44
+ const MANAGED_WASI_PACKAGE_DIRS = [...MANAGED_WASI_PACKAGE_TARGETS.keys()]
24
45
 
25
46
  export interface PackageMeta {
26
47
  'dist-tags': { [index: string]: string }
@@ -28,6 +49,22 @@ export interface PackageMeta {
28
49
 
29
50
  const WASM_RUNTIME_PACKAGE_NAME = '@napi-rs/wasm-runtime'
30
51
 
52
+ interface PendingMetadataWrite {
53
+ content: string
54
+ destination: string
55
+ }
56
+
57
+ interface ManagedPackageDirectory {
58
+ name: string
59
+ path: string
60
+ }
61
+
62
+ interface OwnedWasiPackage {
63
+ binaryName: string
64
+ packageName: string
65
+ target: Target
66
+ }
67
+
31
68
  async function getLatestWasmRuntimeVersion() {
32
69
  const npmRegistryBase =
33
70
  process.env.npm_config_registry?.replace(/\/?$/, '/') ??
@@ -72,57 +109,367 @@ async function getLatestWasmRuntimeVersion() {
72
109
  return latestVersion.trim()
73
110
  }
74
111
 
75
- export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
76
- const options = applyDefaultCreateNpmDirsOptions(userOptions)
112
+ function assertSafeManagedPathSegment(value: string, label: string) {
113
+ if (
114
+ value.length === 0 ||
115
+ value === '.' ||
116
+ value === '..' ||
117
+ value.includes('\0') ||
118
+ value.includes('/') ||
119
+ value.includes('\\') ||
120
+ isAbsolute(value) ||
121
+ basename(value) !== value
122
+ ) {
123
+ throw new Error(
124
+ `${label} must be a single filesystem path segment: ${value}`,
125
+ )
126
+ }
127
+ }
77
128
 
78
- async function mkdirAsync(dir: string) {
79
- debug('Try to create dir: %i', dir)
80
- if (options.dryRun) {
129
+ async function lstatIfExists(path: string) {
130
+ try {
131
+ return await lstat(path)
132
+ } catch (error) {
133
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
81
134
  return
82
135
  }
136
+ throw error
137
+ }
138
+ }
83
139
 
84
- await rawMkdirAsync(dir, {
85
- recursive: true,
86
- })
140
+ async function assertSafeTargetDirectory(path: string) {
141
+ const stats = await lstatIfExists(path)
142
+ if (!stats) {
143
+ return
144
+ }
145
+ if (stats.isSymbolicLink()) {
146
+ throw new Error(
147
+ `npm target directory must not be a symbolic link or junction: ${path}`,
148
+ )
149
+ }
150
+ if (!stats.isDirectory()) {
151
+ throw new Error(`npm target path is not a directory: ${path}`)
87
152
  }
153
+ }
88
154
 
89
- async function writeFileAsync(file: string, content: string) {
90
- debug('Writing file %i', file)
155
+ async function resolveManagedPackageDirectories(
156
+ options: ReturnType<typeof applyDefaultCreateNpmDirsOptions>,
157
+ initialPaths: ReturnType<typeof resolvePackageReconciliationPaths>,
158
+ targets: Target[],
159
+ ) {
160
+ const directoryNames = [
161
+ ...new Set([
162
+ ...targets.map((target) => target.platformArchABI),
163
+ ...MANAGED_WASI_PACKAGE_DIRS,
164
+ ]),
165
+ ]
166
+ for (const directoryName of directoryNames) {
167
+ assertSafeManagedPathSegment(
168
+ directoryName,
169
+ `Target output identity ${directoryName}`,
170
+ )
171
+ }
91
172
 
92
- if (options.dryRun) {
93
- debug(content)
94
- return
173
+ const requestedNpmPath = resolve(options.cwd, options.npmDir)
174
+ const requestedTargetPaths = directoryNames.map((directoryName) =>
175
+ join(requestedNpmPath, directoryName),
176
+ )
177
+ await Promise.all(requestedTargetPaths.map(assertSafeTargetDirectory))
178
+
179
+ const resolvedPaths = resolvePackageReconciliationPaths(
180
+ options.cwd,
181
+ options.packageJsonPath,
182
+ [options.npmDir, ...requestedTargetPaths],
183
+ )
184
+ if (resolvedPaths.boundary !== initialPaths.boundary) {
185
+ throw new Error(
186
+ `Managed npm target paths changed the reconciliation boundary from ${initialPaths.boundary} to ${resolvedPaths.boundary}`,
187
+ )
188
+ }
189
+
190
+ return new Map(
191
+ directoryNames.map((name, index) => [
192
+ name,
193
+ {
194
+ name,
195
+ path: resolvedPaths.managedPaths[index + 1],
196
+ } satisfies ManagedPackageDirectory,
197
+ ]),
198
+ )
199
+ }
200
+
201
+ function managedWasiGeneratedFiles(binaryName: string, packageDir: string) {
202
+ assertSafeManagedPathSegment(binaryName, 'Configured binary name')
203
+ const targetTriple = MANAGED_WASI_PACKAGE_TARGETS.get(packageDir)
204
+ if (!targetTriple) {
205
+ return new Set<string>()
206
+ }
207
+ const target = parseTriple(targetTriple)
208
+ const loaderSuffix = wasiLoaderSuffix(packageDir)
209
+ const files = new Set([
210
+ `${binaryName}.${packageDir}.wasm`,
211
+ `${binaryName}.${packageDir}.debug.wasm`,
212
+ `${binaryName}.${loaderSuffix}.cjs`,
213
+ `${binaryName}.${loaderSuffix}.d.cts`,
214
+ `${binaryName}.${loaderSuffix}-browser.js`,
215
+ ])
216
+ if (wasiTargetHasThreads(target)) {
217
+ files.add('wasi-worker.mjs')
218
+ files.add('wasi-worker-browser.mjs')
219
+ } else {
220
+ for (const file of [
221
+ `${binaryName}.${packageDir}.wasm.d.ts`,
222
+ `${binaryName}.${packageDir}.wasm.d.mts`,
223
+ `${binaryName}.${packageDir}.workerd.mjs`,
224
+ `${binaryName}.${packageDir}.workerd.d.mts`,
225
+ `${binaryName}.${loaderSuffix}-deferred.js`,
226
+ `${binaryName}.${loaderSuffix}-deferred.d.ts`,
227
+ ]) {
228
+ files.add(file)
95
229
  }
230
+ }
231
+ return files
232
+ }
233
+
234
+ function asJsonRecord(value: unknown): Record<string, unknown> | undefined {
235
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
236
+ ? (value as Record<string, unknown>)
237
+ : undefined
238
+ }
96
239
 
97
- await rawWriteFileAsync(file, content)
240
+ async function inspectOwnedWasiPackage(
241
+ directory: ManagedPackageDirectory,
242
+ ): Promise<OwnedWasiPackage | undefined> {
243
+ const targetTriple = MANAGED_WASI_PACKAGE_TARGETS.get(directory.name)
244
+ if (!targetTriple) {
245
+ return
246
+ }
247
+ const manifestPath = join(directory.path, 'package.json')
248
+ const stats = await lstatIfExists(manifestPath)
249
+ if (!stats?.isFile()) {
250
+ return
98
251
  }
99
252
 
100
- const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
101
- const npmPath = resolve(options.cwd, options.npmDir)
253
+ let manifest: Record<string, unknown> | undefined
254
+ try {
255
+ manifest = asJsonRecord(JSON.parse(await readFile(manifestPath, 'utf8')))
256
+ } catch {
257
+ return
258
+ }
259
+ if (!manifest || typeof manifest.name !== 'string') {
260
+ return
261
+ }
102
262
 
263
+ const packageNameSuffix = `-${directory.name}`
264
+ if (
265
+ !manifest.name.endsWith(packageNameSuffix) ||
266
+ manifest.name.length === packageNameSuffix.length ||
267
+ typeof manifest.version !== 'string' ||
268
+ typeof manifest.main !== 'string' ||
269
+ !Array.isArray(manifest.files) ||
270
+ !manifest.files.every((file) => typeof file === 'string') ||
271
+ manifest.type !== 'module'
272
+ ) {
273
+ return
274
+ }
275
+
276
+ const loaderSuffix = wasiLoaderSuffix(directory.name)
277
+ const mainSuffix = `.${loaderSuffix}.cjs`
278
+ if (
279
+ !manifest.main.endsWith(mainSuffix) ||
280
+ manifest.main.length === mainSuffix.length
281
+ ) {
282
+ return
283
+ }
284
+ const binaryName = manifest.main.slice(0, -mainSuffix.length)
285
+ try {
286
+ assertSafeManagedPathSegment(binaryName, 'Managed WASI binary name')
287
+ } catch {
288
+ return
289
+ }
290
+
291
+ const target = parseTriple(targetTriple)
292
+ const requiredFiles = [
293
+ `${binaryName}.${directory.name}.wasm`,
294
+ `${binaryName}.${loaderSuffix}.cjs`,
295
+ `${binaryName}.${loaderSuffix}.d.cts`,
296
+ `${binaryName}.${loaderSuffix}-browser.js`,
297
+ ...(wasiTargetHasThreads(target)
298
+ ? ['wasi-worker.mjs', 'wasi-worker-browser.mjs']
299
+ : [
300
+ `${binaryName}.${loaderSuffix}-deferred.js`,
301
+ `${binaryName}.${loaderSuffix}-deferred.d.ts`,
302
+ `${binaryName}.${directory.name}.wasm.d.ts`,
303
+ ]),
304
+ ]
305
+ const files = new Set(manifest.files)
306
+ if (
307
+ manifest.types !== `${binaryName}.${loaderSuffix}.d.cts` ||
308
+ manifest.browser !== `${binaryName}.${loaderSuffix}-browser.js` ||
309
+ !requiredFiles.every((file) => files.has(file))
310
+ ) {
311
+ return
312
+ }
313
+
314
+ return {
315
+ binaryName,
316
+ packageName: manifest.name.slice(0, -packageNameSuffix.length),
317
+ target,
318
+ }
319
+ }
320
+
321
+ async function collectStaleWasiPackageRemovals(
322
+ directory: ManagedPackageDirectory,
323
+ binaryName: string,
324
+ ) {
325
+ let entries
326
+ try {
327
+ entries = await readdir(directory.path, { withFileTypes: true })
328
+ } catch (error) {
329
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
330
+ return []
331
+ }
332
+ throw error
333
+ }
334
+ const entriesByName = new Map(entries.map((entry) => [entry.name, entry]))
335
+ const owner = await inspectOwnedWasiPackage(directory)
336
+ const generatedFiles = managedWasiGeneratedFiles(binaryName, directory.name)
337
+ if (owner) {
338
+ for (const file of managedWasiGeneratedFiles(
339
+ owner.binaryName,
340
+ directory.name,
341
+ )) {
342
+ generatedFiles.add(file)
343
+ }
344
+ }
345
+
346
+ const removals: string[] = []
347
+ for (const file of generatedFiles) {
348
+ const entry = entriesByName.get(file)
349
+ if (!entry) {
350
+ continue
351
+ }
352
+ if (!entry.isFile()) {
353
+ throw new Error(
354
+ `Managed WASI package file must be a regular file: ${join(directory.path, file)}`,
355
+ )
356
+ }
357
+ removals.push(join(directory.path, file))
358
+ }
359
+
360
+ if (owner) {
361
+ removals.push(join(directory.path, 'package.json'))
362
+ const readmePath = join(directory.path, 'README.md')
363
+ if (
364
+ entriesByName.get('README.md')?.isFile() &&
365
+ (await readFile(readmePath, 'utf8')) ===
366
+ readme(owner.packageName, owner.target)
367
+ ) {
368
+ removals.push(readmePath)
369
+ }
370
+ }
371
+ return removals
372
+ }
373
+
374
+ async function publishPackageMetadata(
375
+ reconciliationRoot: string,
376
+ pendingWrites: PendingMetadataWrite[],
377
+ removals: string[],
378
+ ) {
379
+ const stagingRoot = await mkdtemp(
380
+ join(tmpdir(), 'napi-rs-create-npm-dirs-stage-'),
381
+ )
382
+ try {
383
+ const writes: FileSystemTransactionWrite[] = []
384
+ for (const [index, pendingWrite] of pendingWrites.entries()) {
385
+ const source = join(stagingRoot, String(index))
386
+ await writeFile(source, pendingWrite.content)
387
+ writes.push({
388
+ destination: pendingWrite.destination,
389
+ source,
390
+ })
391
+ }
392
+ if (writes.length > 0 || removals.length > 0) {
393
+ await commitFileSystemTransaction(reconciliationRoot, writes, removals)
394
+ }
395
+ } finally {
396
+ await rm(stagingRoot, { force: true, recursive: true })
397
+ }
398
+ }
399
+
400
+ async function removeEmptyStalePackageDirectories(
401
+ directories: ManagedPackageDirectory[],
402
+ ) {
403
+ for (const directory of directories) {
404
+ try {
405
+ await rmdir(directory.path)
406
+ } catch (error) {
407
+ const code = (error as NodeJS.ErrnoException).code
408
+ if (code !== 'ENOENT' && code !== 'ENOTEMPTY' && code !== 'EEXIST') {
409
+ debug.warn(
410
+ `Failed to remove empty stale npm package directory ${directory.path}: ${String(error)}`,
411
+ )
412
+ }
413
+ }
414
+ }
415
+ }
416
+
417
+ async function createNpmDirsUnlocked(
418
+ options: ReturnType<typeof applyDefaultCreateNpmDirsOptions>,
419
+ initialPaths: ReturnType<typeof resolvePackageReconciliationPaths>,
420
+ ) {
421
+ const packageJsonPath = initialPaths.packageJsonPath
103
422
  debug(`Read content from [${options.configPath ?? packageJsonPath}]`)
104
423
 
105
- const { targets, binaryName, packageName, packageJson } =
424
+ const { targets, binaryName, packageName, packageJson, wasm } =
106
425
  await readNapiConfig(
107
426
  packageJsonPath,
108
427
  options.configPath ? resolve(options.cwd, options.configPath) : undefined,
109
428
  )
429
+ assertSafeManagedPathSegment(binaryName, 'Configured binary name')
430
+ const packageDirectories = await resolveManagedPackageDirectories(
431
+ options,
432
+ initialPaths,
433
+ targets,
434
+ )
435
+ const configuredPackageDirs = new Set(
436
+ targets.map((target) => target.platformArchABI),
437
+ )
438
+ const staleWasiDirectories = MANAGED_WASI_PACKAGE_DIRS.filter(
439
+ (packageDir) => !configuredPackageDirs.has(packageDir),
440
+ ).map((packageDir) => packageDirectories.get(packageDir)!)
441
+ const staleWasiRemovals = (
442
+ await Promise.all(
443
+ staleWasiDirectories.map((directory) =>
444
+ collectStaleWasiPackageRemovals(directory, binaryName),
445
+ ),
446
+ )
447
+ ).flat()
110
448
  const wasmRuntimeVersion = targets.some((target) => target.arch === 'wasm32')
111
449
  ? await getLatestWasmRuntimeVersion()
112
450
  : undefined
451
+ const pendingWrites: PendingMetadataWrite[] = []
113
452
 
114
453
  for (const target of targets) {
115
- const targetDir = join(npmPath, `${target.platformArchABI}`)
116
- await mkdirAsync(targetDir)
454
+ const targetDir = packageDirectories.get(target.platformArchABI)!.path
455
+ debug('Plan npm package dir: %i', targetDir)
117
456
 
118
457
  const binaryFileName =
119
458
  target.arch === 'wasm32'
120
459
  ? `${binaryName}.${target.platformArchABI}.wasm`
121
460
  : `${binaryName}.${target.platformArchABI}.node`
461
+ let wasmModuleTypeDef: string | undefined
122
462
  const scopedPackageJson: CommonPackageJsonFields = {
123
463
  name: `${packageName}-${target.platformArchABI}`,
124
464
  version: packageJson.version,
125
- cpu: target.arch !== 'universal' ? [target.arch] : undefined,
465
+ // WASI modules execute inside a normal host Node/browser/workerd process.
466
+ // Marking them as cpu=wasm32 makes npm reject direct installation and
467
+ // silently skip the package when it is an optional dependency on x64 or
468
+ // arm64 hosts.
469
+ cpu:
470
+ target.arch !== 'universal' && target.arch !== 'wasm32'
471
+ ? [target.arch]
472
+ : undefined,
126
473
  main: binaryFileName,
127
474
  files: [binaryFileName],
128
475
  ...pick(
@@ -148,26 +495,74 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
148
495
  if (target.arch !== 'wasm32') {
149
496
  scopedPackageJson.os = [target.platform]
150
497
  } else {
151
- const entry = `${binaryName}.wasi.cjs`
498
+ const loaderSuffix = wasiLoaderSuffix(target.platformArchABI)
499
+ const entry = `${binaryName}.${loaderSuffix}.cjs`
500
+ const loaderTypeDef = `${binaryName}.${loaderSuffix}.d.cts`
152
501
  scopedPackageJson.main = entry
153
- scopedPackageJson.browser = `${binaryName}.wasi-browser.js`
502
+ scopedPackageJson.types = loaderTypeDef
503
+ scopedPackageJson.browser = `${binaryName}.${loaderSuffix}-browser.js`
504
+ scopedPackageJson.type = 'module'
154
505
  scopedPackageJson.files?.push(
155
506
  entry,
507
+ loaderTypeDef,
156
508
  scopedPackageJson.browser,
157
- `wasi-worker.mjs`,
158
- `wasi-worker-browser.mjs`,
159
509
  )
510
+ if (wasiTargetHasThreads(target)) {
511
+ // worker scripts are only referenced by the threaded loaders
512
+ scopedPackageJson.files?.push(
513
+ `wasi-worker.mjs`,
514
+ `wasi-worker-browser.mjs`,
515
+ )
516
+ } else {
517
+ const deferredEntry = `${binaryName}.${loaderSuffix}-deferred.js`
518
+ const deferredTypeDef = `${binaryName}.${loaderSuffix}-deferred.d.ts`
519
+ wasmModuleTypeDef = `${binaryFileName}.d.ts`
520
+ // the deferred workerd-safe loader is only emitted for non-threaded
521
+ // WASI builds (mirrors `hasThreads` in `writeWasiBinding`)
522
+ scopedPackageJson.files?.push(
523
+ deferredEntry,
524
+ deferredTypeDef,
525
+ wasmModuleTypeDef,
526
+ )
527
+ scopedPackageJson.exports = {
528
+ '.': {
529
+ types: `./${loaderTypeDef}`,
530
+ browser: `./${scopedPackageJson.browser}`,
531
+ require: `./${entry}`,
532
+ default: `./${entry}`,
533
+ },
534
+ './workerd': {
535
+ types: `./${deferredTypeDef}`,
536
+ default: `./${deferredEntry}`,
537
+ },
538
+ './wasm': {
539
+ types: `./${wasmModuleTypeDef}`,
540
+ default: `./${binaryFileName}`,
541
+ },
542
+ './wasm.wasm': {
543
+ types: `./${wasmModuleTypeDef}`,
544
+ default: `./${binaryFileName}`,
545
+ },
546
+ './package.json': './package.json',
547
+ }
548
+ }
160
549
  scopedPackageJson.engines = {
161
550
  ...scopedPackageJson.engines,
162
551
  node: scopedPackageJson.engines?.node
163
552
  ? restrictWasiNodeEngine(scopedPackageJson.engines.node)
164
- : minimumWasiNodeVersion,
553
+ : MINIMUM_WASI_NODE_VERSION,
165
554
  }
166
555
  const emnapiVersion = require('emnapi/package.json').version
167
556
  scopedPackageJson.dependencies = {
168
- '@napi-rs/wasm-runtime': `^${wasmRuntimeVersion}`,
557
+ // Runtime minor releases can target a different emnapi generation.
558
+ // Keep generated packages on the resolved minor while allowing fixes.
559
+ '@napi-rs/wasm-runtime': `~${wasmRuntimeVersion}`,
169
560
  '@emnapi/core': emnapiVersion,
170
561
  '@emnapi/runtime': emnapiVersion,
562
+ ...(wasm?.browser?.buffer === true &&
563
+ (wasm.browser.fs !== true || !wasiTargetHasThreads(target))
564
+ ? { buffer: directBufferDependency }
565
+ : {}),
171
566
  }
172
567
  }
173
568
 
@@ -178,86 +573,64 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
178
573
  }
179
574
 
180
575
  const targetPackageJson = join(targetDir, 'package.json')
181
- await writeFileAsync(
182
- targetPackageJson,
183
- JSON.stringify(scopedPackageJson, null, 2) + '\n',
184
- )
576
+ pendingWrites.push({
577
+ content: JSON.stringify(scopedPackageJson, null, 2) + '\n',
578
+ destination: targetPackageJson,
579
+ })
580
+ if (wasmModuleTypeDef) {
581
+ pendingWrites.push({
582
+ content: createWasmModuleTypeDef(),
583
+ destination: join(targetDir, wasmModuleTypeDef),
584
+ })
585
+ }
185
586
  const targetReadme = join(targetDir, 'README.md')
186
- await writeFileAsync(targetReadme, readme(packageName, target))
587
+ pendingWrites.push({
588
+ content: readme(packageName, target),
589
+ destination: targetReadme,
590
+ })
187
591
 
188
592
  debug.info(`${packageName} -${target.platformArchABI} created`)
189
593
  }
190
- }
191
594
 
192
- function readme(packageName: string, target: Target) {
193
- return `# \`${packageName}-${target.platformArchABI}\`
194
-
195
- This is the **${target.triple}** binary for \`${packageName}\`
196
- `
197
- }
198
-
199
- function restrictWasiNodeEngine(nodeRange: string) {
200
- try {
201
- if (subset(nodeRange, minimumWasiNodeVersion)) {
202
- return nodeRange
203
- }
204
-
205
- if (subset(minimumWasiNodeVersion, nodeRange)) {
206
- return minimumWasiNodeVersion
207
- }
208
-
209
- const minimumComparator = new Comparator(minimumWasiNodeVersion)
210
- const restrictedRangeSets = new Range(nodeRange).set
211
- .map((comparators) =>
212
- normalizeComparatorSet([...comparators, minimumComparator]),
213
- )
214
- .filter(
215
- (candidate): candidate is string =>
216
- candidate !== undefined && minVersion(candidate) !== null,
217
- )
218
-
219
- if (restrictedRangeSets.length > 0) {
220
- return restrictedRangeSets.join(' || ')
595
+ for (const { content, destination } of pendingWrites) {
596
+ debug('Writing file %i', destination)
597
+ if (options.dryRun) {
598
+ debug(content)
221
599
  }
222
- } catch {
223
- // ignore
600
+ }
601
+ for (const removal of staleWasiRemovals) {
602
+ debug('Removing stale managed file %i', removal)
603
+ }
604
+ if (options.dryRun) {
605
+ return
224
606
  }
225
607
 
226
- return minimumWasiNodeVersion
608
+ await publishPackageMetadata(
609
+ initialPaths.boundary,
610
+ pendingWrites,
611
+ staleWasiRemovals,
612
+ )
613
+ await removeEmptyStalePackageDirectories(staleWasiDirectories)
227
614
  }
228
615
 
229
- function normalizeComparatorSet(comparators: Comparator[]) {
230
- const exactMatch = comparators.find(({ operator }) => operator === '')
231
- if (exactMatch) {
232
- return comparators.every((comparator) => comparator.test(exactMatch.semver))
233
- ? exactMatch.value
234
- : undefined
235
- }
236
-
237
- let lowerBound: Comparator | undefined
238
- let upperBound: Comparator | undefined
239
-
240
- for (const comparator of comparators) {
241
- if (comparator.operator === '>' || comparator.operator === '>=') {
242
- if (
243
- !lowerBound ||
244
- comparator.semver.compare(lowerBound.semver) > 0 ||
245
- (comparator.semver.compare(lowerBound.semver) === 0 &&
246
- comparator.operator === '>')
247
- ) {
248
- lowerBound = comparator
249
- }
250
- } else if (comparator.operator === '<' || comparator.operator === '<=') {
251
- if (
252
- !upperBound ||
253
- comparator.semver.compare(upperBound.semver) < 0 ||
254
- (comparator.semver.compare(upperBound.semver) === 0 &&
255
- comparator.operator === '<')
256
- ) {
257
- upperBound = comparator
258
- }
259
- }
616
+ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
617
+ const options = applyDefaultCreateNpmDirsOptions(userOptions)
618
+ const resolvedPaths = resolvePackageReconciliationPaths(
619
+ options.cwd,
620
+ options.packageJsonPath,
621
+ [options.npmDir],
622
+ )
623
+ if (options.dryRun) {
624
+ return createNpmDirsUnlocked(options, resolvedPaths)
260
625
  }
626
+ return withFileSystemReconciliation(resolvedPaths.boundary, () =>
627
+ createNpmDirsUnlocked(options, resolvedPaths),
628
+ )
629
+ }
261
630
 
262
- return [lowerBound?.value, upperBound?.value].filter(Boolean).join(' ')
631
+ function readme(packageName: string, target: Target) {
632
+ return `# \`${packageName}-${target.platformArchABI}\`
633
+
634
+ This is the **${target.triple}** binary for \`${packageName}\`
635
+ `
263
636
  }