@napi-rs/cli 3.9.0 → 3.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napi-rs/cli",
3
- "version": "3.9.0",
3
+ "version": "3.9.1",
4
4
  "description": "Cli tools for napi-rs",
5
5
  "author": "LongYinan <lynweklm@gmail.com>",
6
6
  "homepage": "https://napi.rs/",
@@ -68,7 +68,7 @@
68
68
  "clipanion": "^4.0.0-rc.4",
69
69
  "colorette": "^2.0.20",
70
70
  "es-toolkit": "^1.47.0",
71
- "js-yaml": "^4.2.0",
71
+ "js-yaml": "^5.0.0",
72
72
  "obug": "^2.1.2",
73
73
  "semver": "^7.8.2",
74
74
  "typanion": "^3.14.0",
@@ -87,9 +87,8 @@
87
87
  "emnapi": "^2.0.0-alpha.4",
88
88
  "empathic": "^2.0.1",
89
89
  "env-paths": "^4.0.0",
90
- "oxc-parser": "^0.148.0",
91
- "prettier": "^3.8.3",
92
- "tsdown": "^0.22.2",
90
+ "oxc-parser": "^0.149.0",
91
+ "tsdown": "^0.23.0",
93
92
  "tslib": "^2.8.1"
94
93
  },
95
94
  "peerDependencies": {
@@ -61,11 +61,11 @@ Generated by [AVA](https://avajs.dev).
61
61
  let __emnapiWasmEnvCleanupDrainPromise␊
62
62
  let __wasiDisposed = false␊
63
63
  let __wasiDisposePromise␊
64
- let __completeWasiDisposal = function() {}␊
64
+ let __completeWasiDisposal = function () {}␊
65
65
  // Overridden by loader flavors that have a last-resort reclaim for a rollback␊
66
66
  // that stopped short of destroying the context. See␊
67
67
  // \`__rollbackWasiInitialization\`.␊
68
- let __retainWasiRollbackForRetry = function() {}␊
68
+ let __retainWasiRollbackForRetry = function () {}␊
69
69
  ␊
70
70
  function __isThenable(value) {␊
71
71
  return (␊
@@ -669,11 +669,11 @@ Generated by [AVA](https://avajs.dev).
669
669
  let __emnapiWasmEnvCleanupDrainPromise␊
670
670
  let __wasiDisposed = false␊
671
671
  let __wasiDisposePromise␊
672
- let __completeWasiDisposal = function() {}␊
672
+ let __completeWasiDisposal = function () {}␊
673
673
  // Overridden by loader flavors that have a last-resort reclaim for a rollback␊
674
674
  // that stopped short of destroying the context. See␊
675
675
  // \`__rollbackWasiInitialization\`.␊
676
- let __retainWasiRollbackForRetry = function() {}␊
676
+ let __retainWasiRollbackForRetry = function () {}␊
677
677
  ␊
678
678
  function __isThenable(value) {␊
679
679
  return (␊
@@ -1296,11 +1296,11 @@ Generated by [AVA](https://avajs.dev).
1296
1296
  let __emnapiWasmEnvCleanupDrainPromise␊
1297
1297
  let __wasiDisposed = false␊
1298
1298
  let __wasiDisposePromise␊
1299
- let __completeWasiDisposal = function() {}␊
1299
+ let __completeWasiDisposal = function () {}␊
1300
1300
  // Overridden by loader flavors that have a last-resort reclaim for a rollback␊
1301
1301
  // that stopped short of destroying the context. See␊
1302
1302
  // \`__rollbackWasiInitialization\`.␊
1303
- let __retainWasiRollbackForRetry = function() {}␊
1303
+ let __retainWasiRollbackForRetry = function () {}␊
1304
1304
  ␊
1305
1305
  function __isThenable(value) {␊
1306
1306
  return (␊
@@ -1880,10 +1880,9 @@ Generated by [AVA](https://avajs.dev).
1880
1880
  // eslint-disable-next-line no-console␊
1881
1881
  console.log.apply(console, arguments)␊
1882
1882
  },␊
1883
- printErr: function() {␊
1883
+ printErr: function () {␊
1884
1884
  // eslint-disable-next-line no-console␊
1885
1885
  console.error.apply(console, arguments)␊
1886
- ␊
1887
1886
  },␊
1888
1887
  })␊
1889
1888
  return instantiateNapiModuleSync(wasmModule, {␊
@@ -1904,7 +1903,6 @@ Generated by [AVA](https://avajs.dev).
1904
1903
  },␊
1905
1904
  })␊
1906
1905
  },␊
1907
- ␊
1908
1906
  })␊
1909
1907
  ␊
1910
1908
  globalThis.onmessage = function (e) {␊
@@ -1933,10 +1931,9 @@ Generated by [AVA](https://avajs.dev).
1933
1931
  // eslint-disable-next-line no-console␊
1934
1932
  console.log.apply(console, arguments)␊
1935
1933
  },␊
1936
- printErr: function() {␊
1934
+ printErr: function () {␊
1937
1935
  // eslint-disable-next-line no-console␊
1938
1936
  console.error.apply(console, arguments)␊
1939
- ␊
1940
1937
  errorOutputs.push([...arguments])␊
1941
1938
  },␊
1942
1939
  })␊
@@ -1998,10 +1995,9 @@ Generated by [AVA](https://avajs.dev).
1998
1995
  // eslint-disable-next-line no-console␊
1999
1996
  console.log.apply(console, arguments)␊
2000
1997
  },␊
2001
- printErr: function() {␊
1998
+ printErr: function () {␊
2002
1999
  // eslint-disable-next-line no-console␊
2003
2000
  console.error.apply(console, arguments)␊
2004
- ␊
2005
2001
  errorOutputs.push([...arguments])␊
2006
2002
  },␊
2007
2003
  })␊
@@ -25,9 +25,11 @@ import ava, { type ExecutionContext, type TestFn } from 'ava'
25
25
 
26
26
  import {
27
27
  buildProject,
28
+ EMNAPI_WASI_SDK_34_LINK_DIR,
28
29
  generateTypeDef,
29
30
  napiCrossToolchainEnvs,
30
31
  resolveBuildFormat,
32
+ selectEmnapiLinkDir,
31
33
  validateCrossCompileFlags,
32
34
  validateNapiCrossSupport,
33
35
  writeJsBinding,
@@ -1108,3 +1110,195 @@ const isNapiCrossUnsupportedHost =
1108
1110
  )
1109
1111
  },
1110
1112
  )
1113
+
1114
+ async function createEmnapiLibDir(root: string, dirNames: string[]) {
1115
+ const emnapiLibDir = join(root, 'emnapi', 'lib')
1116
+ for (const dirName of dirNames) {
1117
+ await mkdir(join(emnapiLibDir, dirName), { recursive: true })
1118
+ }
1119
+ return emnapiLibDir
1120
+ }
1121
+
1122
+ async function createWasiSdkDir(root: string, version: string) {
1123
+ const wasiSdkPath = join(root, `wasi-sdk-${version}`)
1124
+ await mkdir(wasiSdkPath, { recursive: true })
1125
+ await writeFile(join(wasiSdkPath, 'VERSION'), `${version}\n`)
1126
+ return wasiSdkPath
1127
+ }
1128
+
1129
+ async function createWasiLibcArchive(
1130
+ projectDir: string,
1131
+ abi: 'legacy' | 'new',
1132
+ ) {
1133
+ const libcPath = join(projectDir, `libc-${abi}.a`)
1134
+ // Only the archive member names matter: wasi-libc moved the futex helpers
1135
+ // into `futex.c` when it dropped the `int op` parameter, and kept `__wait.c`
1136
+ // for other symbols.
1137
+ const members =
1138
+ abi === 'new'
1139
+ ? ['__wait.c.obj', 'futex.c.obj']
1140
+ : ['__wait.c.obj', '__wasilibc_busywait.c.obj']
1141
+ await writeFile(libcPath, `!<arch>\n${members.join('\n')}\n`)
1142
+ return libcPath
1143
+ }
1144
+
1145
+ test('selects the wasi-sdk 34 emnapi archives for wasi-sdk >= 34', async (t) => {
1146
+ const { projectDir } = t.context
1147
+ const emnapiLibDir = await createEmnapiLibDir(projectDir, [
1148
+ 'wasm32-wasip1',
1149
+ 'wasm32-wasip1-threads',
1150
+ EMNAPI_WASI_SDK_34_LINK_DIR,
1151
+ ])
1152
+ const wasiSdkPath = await createWasiSdkDir(projectDir, '34.0')
1153
+
1154
+ t.deepEqual(
1155
+ selectEmnapiLinkDir(
1156
+ emnapiLibDir,
1157
+ 'wasm32-wasip1-threads',
1158
+ true,
1159
+ wasiSdkPath,
1160
+ ),
1161
+ {
1162
+ linkDirName: EMNAPI_WASI_SDK_34_LINK_DIR,
1163
+ wasiSdkMajor: 34,
1164
+ needsWasiSdk34: true,
1165
+ },
1166
+ )
1167
+ })
1168
+
1169
+ test('keeps the legacy emnapi archives for wasi-sdk <= 33', async (t) => {
1170
+ const { projectDir } = t.context
1171
+ const emnapiLibDir = await createEmnapiLibDir(projectDir, [
1172
+ 'wasm32-wasip1-threads',
1173
+ EMNAPI_WASI_SDK_34_LINK_DIR,
1174
+ ])
1175
+ const wasiSdkPath = await createWasiSdkDir(projectDir, '33.0')
1176
+
1177
+ t.deepEqual(
1178
+ selectEmnapiLinkDir(
1179
+ emnapiLibDir,
1180
+ 'wasm32-wasip1-threads',
1181
+ true,
1182
+ wasiSdkPath,
1183
+ ),
1184
+ {
1185
+ linkDirName: 'wasm32-wasip1-threads',
1186
+ wasiSdkMajor: 33,
1187
+ needsWasiSdk34: false,
1188
+ },
1189
+ )
1190
+ })
1191
+
1192
+ test('keeps the legacy emnapi archives when the wasi-libc ABI is unknown', async (t) => {
1193
+ const { projectDir } = t.context
1194
+ const emnapiLibDir = await createEmnapiLibDir(projectDir, [
1195
+ 'wasm32-wasip1-threads',
1196
+ EMNAPI_WASI_SDK_34_LINK_DIR,
1197
+ ])
1198
+
1199
+ t.deepEqual(
1200
+ // `null` stands in for a Rust sysroot that cannot be probed.
1201
+ selectEmnapiLinkDir(
1202
+ emnapiLibDir,
1203
+ 'wasm32-wasip1-threads',
1204
+ true,
1205
+ undefined,
1206
+ { rustWasiLibc: null },
1207
+ ),
1208
+ {
1209
+ linkDirName: 'wasm32-wasip1-threads',
1210
+ wasiSdkMajor: null,
1211
+ needsWasiSdk34: false,
1212
+ },
1213
+ )
1214
+ })
1215
+
1216
+ test('keeps the legacy emnapi archives for a pre wasi-sdk 34 Rust toolchain', async (t) => {
1217
+ const { projectDir } = t.context
1218
+ const emnapiLibDir = await createEmnapiLibDir(projectDir, [
1219
+ 'wasm32-wasip1-threads',
1220
+ EMNAPI_WASI_SDK_34_LINK_DIR,
1221
+ ])
1222
+ const rustWasiLibc = await createWasiLibcArchive(projectDir, 'legacy')
1223
+
1224
+ t.deepEqual(
1225
+ selectEmnapiLinkDir(
1226
+ emnapiLibDir,
1227
+ 'wasm32-wasip1-threads',
1228
+ true,
1229
+ undefined,
1230
+ { rustWasiLibc },
1231
+ ),
1232
+ {
1233
+ linkDirName: 'wasm32-wasip1-threads',
1234
+ wasiSdkMajor: null,
1235
+ needsWasiSdk34: false,
1236
+ },
1237
+ )
1238
+ })
1239
+
1240
+ test('selects the wasi-sdk 34 archives for a Rust toolchain that bundles the new wasi-libc', async (t) => {
1241
+ const { projectDir } = t.context
1242
+ const emnapiLibDir = await createEmnapiLibDir(projectDir, [
1243
+ 'wasm32-wasip1-threads',
1244
+ EMNAPI_WASI_SDK_34_LINK_DIR,
1245
+ ])
1246
+ const rustWasiLibc = await createWasiLibcArchive(projectDir, 'new')
1247
+
1248
+ t.deepEqual(
1249
+ // No wasi-sdk is configured: cargo links Rust's bundled wasi-libc, and
1250
+ // Rust picked up wasi-sdk 34 in rust-lang/rust#161773.
1251
+ selectEmnapiLinkDir(
1252
+ emnapiLibDir,
1253
+ 'wasm32-wasip1-threads',
1254
+ true,
1255
+ undefined,
1256
+ { rustWasiLibc },
1257
+ ),
1258
+ {
1259
+ linkDirName: EMNAPI_WASI_SDK_34_LINK_DIR,
1260
+ wasiSdkMajor: null,
1261
+ needsWasiSdk34: true,
1262
+ },
1263
+ )
1264
+ })
1265
+
1266
+ test('never selects the wasi-sdk 34 archives for the threadless target', async (t) => {
1267
+ const { projectDir } = t.context
1268
+ const emnapiLibDir = await createEmnapiLibDir(projectDir, [
1269
+ 'wasm32-wasip1',
1270
+ EMNAPI_WASI_SDK_34_LINK_DIR,
1271
+ ])
1272
+ const wasiSdkPath = await createWasiSdkDir(projectDir, '34.0')
1273
+
1274
+ t.deepEqual(
1275
+ selectEmnapiLinkDir(emnapiLibDir, 'wasm32-wasip1', false, wasiSdkPath),
1276
+ {
1277
+ linkDirName: 'wasm32-wasip1',
1278
+ wasiSdkMajor: 34,
1279
+ needsWasiSdk34: false,
1280
+ },
1281
+ )
1282
+ })
1283
+
1284
+ test('falls back to the legacy archives when emnapi has no wasi-sdk 34 directory', async (t) => {
1285
+ const { projectDir } = t.context
1286
+ const emnapiLibDir = await createEmnapiLibDir(projectDir, [
1287
+ 'wasm32-wasip1-threads',
1288
+ ])
1289
+ const wasiSdkPath = await createWasiSdkDir(projectDir, '34.0')
1290
+
1291
+ t.deepEqual(
1292
+ selectEmnapiLinkDir(
1293
+ emnapiLibDir,
1294
+ 'wasm32-wasip1-threads',
1295
+ true,
1296
+ wasiSdkPath,
1297
+ ),
1298
+ {
1299
+ linkDirName: 'wasm32-wasip1-threads',
1300
+ wasiSdkMajor: 34,
1301
+ needsWasiSdk34: true,
1302
+ },
1303
+ )
1304
+ })
@@ -131,9 +131,10 @@ test('should omit exports fields from publishConfig in scoped packages', async (
131
131
  const scopedPackageJsonPath = join(scopedDir, 'package.json')
132
132
  t.true(existsSync(scopedPackageJsonPath))
133
133
 
134
- const scopedPackageJson = JSON.parse(
135
- await readFile(scopedPackageJsonPath, 'utf-8'),
136
- )
134
+ const scopedPackageJsonSource = await readFile(scopedPackageJsonPath, 'utf-8')
135
+ t.true(scopedPackageJsonSource.endsWith('\n'))
136
+ t.false(scopedPackageJsonSource.endsWith('\n\n'))
137
+ const scopedPackageJson = JSON.parse(scopedPackageJsonSource)
137
138
 
138
139
  // Verify that publishConfig only contains registry and access, not exports
139
140
  t.truthy(scopedPackageJson.publishConfig)
@@ -7,7 +7,10 @@ import {
7
7
  createWasiBrowserBinding,
8
8
  createWasiDeferredBrowserBinding,
9
9
  } from '../templates/load-wasi-template.js'
10
- import { createWasiBrowserWorkerBinding } from '../templates/wasi-worker-template.js'
10
+ import {
11
+ createWasiBrowserWorkerBinding,
12
+ WASI_WORKER_TEMPLATE,
13
+ } from '../templates/wasi-worker-template.js'
11
14
 
12
15
  const test = ava
13
16
 
@@ -228,6 +231,36 @@ const wasiLoaderCases: Array<{ name: string; code: string }> = [
228
231
  { name: 'deferred/workerd', code: createWasiDeferredBrowserBinding('test') },
229
232
  ]
230
233
 
234
+ test('Node WASI loader uses an accessible host root on Android', (t) => {
235
+ const code = createWasiBinding('test', '@scope/test')
236
+ assertValidJS(t, code, 'Node WASI loader')
237
+ t.true(code.includes('const __cwd = process.cwd()'))
238
+ t.true(code.includes("process.platform === 'android' ? __cwd : __rootDir"))
239
+ t.true(code.includes('[__rootDir]: __hostRoot'))
240
+ t.true(code.includes('[__hostRoot]: __hostRoot'))
241
+ t.true(
242
+ code.includes('workerData: { hostRoot: __hostRoot, rootDir: __rootDir }'),
243
+ )
244
+ t.false(code.includes('[__rootDir]: __rootDir'))
245
+ })
246
+
247
+ test('Node WASI worker uses an accessible host root on Android', (t) => {
248
+ assertValidJS(t, WASI_WORKER_TEMPLATE, 'Node WASI worker')
249
+ t.true(
250
+ WASI_WORKER_TEMPLATE.includes(
251
+ "workerData && typeof workerData.rootDir === 'string' && workerData.rootDir",
252
+ ),
253
+ )
254
+ t.true(
255
+ WASI_WORKER_TEMPLATE.includes(
256
+ "workerData && typeof workerData.hostRoot === 'string' && workerData.hostRoot",
257
+ ),
258
+ )
259
+ t.true(WASI_WORKER_TEMPLATE.includes('[__rootDir]: __hostRoot'))
260
+ t.true(WASI_WORKER_TEMPLATE.includes('[__hostRoot]: __hostRoot'))
261
+ t.false(WASI_WORKER_TEMPLATE.includes('[__rootDir]: __rootDir'))
262
+ })
263
+
231
264
  for (const { name, code } of wasiLoaderCases) {
232
265
  test(`WASI loader waits for queued settlements before destroy: ${name}`, (t) => {
233
266
  t.true(
@@ -284,6 +317,34 @@ function initializationRollbackBody(code: string): string {
284
317
  return code.slice(deferredStart, code.indexOf('throw error', deferredStart))
285
318
  }
286
319
 
320
+ test('createCjsBinding uses one statement dialect', (t) => {
321
+ const code = createCjsBinding('test', '@scope/test', ['sum'], '1.0.0', [
322
+ 'wasm32-wasi',
323
+ ])
324
+ t.false(
325
+ code.includes('NAPI_RS_NATIVE_LIBRARY_PATH);'),
326
+ 'native library require must not use a leftover semicolon',
327
+ )
328
+ t.true(code.includes("const __napiWasiFlavors = ['wasm32-wasi']"))
329
+ t.false(code.includes('"wasm32-wasi"'))
330
+ t.true(code.includes("return require('./test.win32-x64-gnu.node')"))
331
+ const win32Gnu = code.slice(
332
+ code.indexOf("process.arch === 'x64'"),
333
+ code.indexOf("process.arch === 'ia32'"),
334
+ )
335
+ t.true(
336
+ win32Gnu.includes(" return require('./test.win32-x64-gnu.node')"),
337
+ 'win32-x64 gnu local require must be indented inside try',
338
+ )
339
+ })
340
+
341
+ test('WASI worker template matches the CJS/ESM quote and semicolon dialect', (t) => {
342
+ t.true(WASI_WORKER_TEMPLATE.includes("import fs from 'node:fs'"))
343
+ t.false(WASI_WORKER_TEMPLATE.includes('from "node:fs"'))
344
+ t.false(WASI_WORKER_TEMPLATE.includes("from 'node:fs';"))
345
+ t.true(WASI_WORKER_TEMPLATE.includes('memory: wasmMemory,'))
346
+ })
347
+
287
348
  test('createEsmBinding is Node 12 compatible', (t) => {
288
349
  const code = createEsmBinding('test', '@scope/test', ['sum'])
289
350
  assertValidJS(t, code, 'esm')
package/src/api/build.ts CHANGED
@@ -45,7 +45,10 @@ import {
45
45
  targetToEnvVar,
46
46
  tryInstallCargoBinary,
47
47
  unlinkAsync,
48
+ rustBundledWasiLibc,
49
+ wasiLibcHasNewFutexAbi,
48
50
  wasiLoaderSuffix,
51
+ wasiSdkMajorVersion,
49
52
  wasiTargetHasThreads,
50
53
  writeFileAtomic,
51
54
  withFileSystemReconciliation,
@@ -223,6 +226,83 @@ export function getTypeDefCacheFolder(options: {
223
226
  return join(options.targetDir, 'napi-rs', `${options.crateName}-${hash}`)
224
227
  }
225
228
 
229
+ /**
230
+ * Name of the emnapi archive directory built against the wasi-sdk 34 ABI.
231
+ * Only the threaded target has one: the signature change it carries lives in
232
+ * the threads-only futex code.
233
+ */
234
+ export const EMNAPI_WASI_SDK_34_LINK_DIR = 'wasm32-wasip1-threads-wasi-sdk-34'
235
+
236
+ export interface EmnapiLinkDirSelection {
237
+ /** Directory name under `emnapi/lib` whose archives should be linked. */
238
+ linkDirName: string
239
+ /** Detected wasi-sdk major version, `null` when no wasi-sdk is configured. */
240
+ wasiSdkMajor: number | null
241
+ /** `true` when the toolchain needs the wasi-sdk 34 archives. */
242
+ needsWasiSdk34: boolean
243
+ }
244
+
245
+ /**
246
+ * Pick the emnapi archive directory for a WASI build.
247
+ *
248
+ * wasi-libc removed the unused `int op` parameter from
249
+ * `__wasilibc_futex_wait_atomic_wait` / `__wasilibc_futex_wait_maybe_busy`,
250
+ * and wasi-sdk 34 is the first release shipping the 3-argument signature.
251
+ * Linking the legacy 4-argument archives against a wasi-sdk >= 34 sysroot
252
+ * makes `wasm-ld` report `function signature mismatch` and emit an invalid
253
+ * wasm module, so emnapi publishes a second archive set for the new ABI and
254
+ * the directory has to be chosen by wasi-sdk version, not by target triple.
255
+ *
256
+ * Without `WASI_SDK_PATH`, cargo links through `rust-lld` against the
257
+ * wasi-libc bundled with the Rust standard library, so that archive is
258
+ * probed instead. Rust picked up wasi-sdk 34 in rust-lang/rust#161773, which
259
+ * lands in 1.100, and a toolchain carrying it needs the new archives even
260
+ * though no wasi-sdk is configured.
261
+ *
262
+ * The legacy directory stays the default whenever the ABI cannot be
263
+ * determined, and older emnapi releases ship it alone.
264
+ */
265
+ export function selectEmnapiLinkDir(
266
+ emnapiLibDir: string,
267
+ wasiTarget: string,
268
+ hasThreads: boolean,
269
+ wasiSdkPath: string | undefined,
270
+ {
271
+ cwd,
272
+ rustWasiLibc,
273
+ }: {
274
+ // The directory Cargo is spawned in. It selects the toolchain, so the
275
+ // sysroot probe has to use it too.
276
+ cwd?: string
277
+ // Omit to resolve the Rust sysroot lazily, so a configured wasi-sdk never
278
+ // pays for a `rustc` invocation. Pass `null` to skip the probe.
279
+ rustWasiLibc?: string | null
280
+ } = {},
281
+ ): EmnapiLinkDirSelection {
282
+ const usingWasiSdk = Boolean(wasiSdkPath) && existsSync(wasiSdkPath!)
283
+ const wasiSdkMajor = usingWasiSdk ? wasiSdkMajorVersion(wasiSdkPath!) : null
284
+ // Whichever wasi-libc actually gets linked decides the ABI: the wasi-sdk
285
+ // sysroot when one is configured, otherwise the copy bundled with the Rust
286
+ // standard library. Only the threaded target has a second archive set, so
287
+ // `hasThreads` gates the probe as well as the result — a threadless build
288
+ // must not pay for a `rustc` invocation and an archive read it cannot use.
289
+ const needsWasiSdk34 =
290
+ hasThreads &&
291
+ (usingWasiSdk
292
+ ? wasiSdkMajor !== null && wasiSdkMajor >= 34
293
+ : wasiLibcHasNewFutexAbi(
294
+ rustWasiLibc === undefined
295
+ ? rustBundledWasiLibc(wasiTarget, cwd)
296
+ : rustWasiLibc,
297
+ ) === true)
298
+ const linkDirName =
299
+ needsWasiSdk34 &&
300
+ existsSync(join(emnapiLibDir, EMNAPI_WASI_SDK_34_LINK_DIR))
301
+ ? EMNAPI_WASI_SDK_34_LINK_DIR
302
+ : wasiTarget
303
+ return { linkDirName, wasiSdkMajor, needsWasiSdk34 }
304
+ }
305
+
226
306
  export function createWasiCompilerFlags(
227
307
  wasiSdkPath: string,
228
308
  wasiTarget: string,
@@ -1461,16 +1541,40 @@ class Builder {
1461
1541
  private setWasiEnv() {
1462
1542
  const hasThreads = wasiTargetHasThreads(this.target)
1463
1543
  const wasiTarget = hasThreads ? 'wasm32-wasip1-threads' : 'wasm32-wasip1'
1464
- const emnapi = join(require.resolve('emnapi'), '..', 'lib', wasiTarget)
1544
+ const emnapiLibDir = join(require.resolve('emnapi'), '..', 'lib')
1465
1545
  const emnapiVersion = require('emnapi/package.json').version
1546
+ const { WASI_SDK_PATH } = process.env
1547
+ // `wasiTarget` stays the real target triple, because it is what the clang
1548
+ // driver is told to build for below. The emnapi archive directory is a
1549
+ // separate choice: emnapi ships two archive sets for the threaded target,
1550
+ // one per wasi-libc futex ABI. See `selectEmnapiLinkDir`.
1551
+ const { linkDirName, wasiSdkMajor, needsWasiSdk34 } = selectEmnapiLinkDir(
1552
+ emnapiLibDir,
1553
+ wasiTarget,
1554
+ hasThreads,
1555
+ WASI_SDK_PATH,
1556
+ { cwd: this.options.cwd },
1557
+ )
1558
+ const emnapi = join(emnapiLibDir, linkDirName)
1466
1559
  // Keep this in sync with `emnapi_link_library` in `crates/build/src/wasi.rs`.
1467
1560
  const emnapiArchive = join(
1468
1561
  emnapi,
1469
1562
  hasThreads ? 'libemnapi-napi-rs-mt.a' : 'libemnapi-basic-napi-rs.a',
1470
1563
  )
1564
+ const fellBackToLegacy =
1565
+ needsWasiSdk34 && linkDirName !== EMNAPI_WASI_SDK_34_LINK_DIR
1471
1566
  if (!existsSync(emnapiArchive)) {
1472
1567
  throw new Error(
1473
- `emnapi@${emnapiVersion} is missing the ${wasiTarget} archive required by napi-rs at ${emnapiArchive}. Install emnapi v2 with support for this target.`,
1568
+ fellBackToLegacy
1569
+ ? `emnapi@${emnapiVersion} does not ship the ${EMNAPI_WASI_SDK_34_LINK_DIR} archives that wasi-sdk ${wasiSdkMajor} requires, and the ${linkDirName} archive it fell back to is missing too at ${emnapiArchive}. Upgrade emnapi to a version that ships the wasi-sdk 34 archives.`
1570
+ : needsWasiSdk34
1571
+ ? `emnapi@${emnapiVersion} is missing the ${linkDirName} archive required by napi-rs at ${emnapiArchive}. wasi-sdk ${wasiSdkMajor} needs those archives, so upgrade emnapi to a version that ships them.`
1572
+ : `emnapi@${emnapiVersion} is missing the ${linkDirName} archive required by napi-rs at ${emnapiArchive}. Install emnapi v2 with support for this target.`,
1573
+ )
1574
+ }
1575
+ if (fellBackToLegacy) {
1576
+ debug.warn(
1577
+ `emnapi@${emnapiVersion} does not ship the ${EMNAPI_WASI_SDK_34_LINK_DIR} archives that wasi-sdk ${wasiSdkMajor} requires. Falling back to ${linkDirName}, which links the legacy wasi-libc futex ABI and may fail with \`wasm-ld: function signature mismatch\`. Upgrade emnapi to fix this.`,
1474
1578
  )
1475
1579
  }
1476
1580
  this.envs.EMNAPI_LINK_DIR = emnapi
@@ -1499,7 +1603,6 @@ class Builder {
1499
1603
  `emnapi version mismatch: emnapi@${emnapiVersion}, @emnapi/core@${emnapiCoreVersion}, @emnapi/runtime@${emnapiRuntimeVersion}. Please ensure all emnapi packages are the same version.`,
1500
1604
  )
1501
1605
  }
1502
- const { WASI_SDK_PATH } = process.env
1503
1606
 
1504
1607
  if (WASI_SDK_PATH && existsSync(WASI_SDK_PATH)) {
1505
1608
  this.envs.CARGO_TARGET_WASM32_WASI_PREVIEW1_THREADS_LINKER = join(
@@ -2560,10 +2663,27 @@ export declare class ExternalObject<T> {
2560
2663
 
2561
2664
  if (dts.indexOf('TypedArray') > -1) {
2562
2665
  header += `
2563
- export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array
2666
+ export type TypedArray =
2667
+ | Int8Array
2668
+ | Uint8Array
2669
+ | Uint8ClampedArray
2670
+ | Int16Array
2671
+ | Uint16Array
2672
+ | Int32Array
2673
+ | Uint32Array
2674
+ | Float32Array
2675
+ | Float64Array
2676
+ | BigInt64Array
2677
+ | BigUint64Array
2564
2678
  `
2565
2679
  }
2566
2680
 
2681
+ if (header && !header.endsWith('\n')) {
2682
+ header += '\n'
2683
+ }
2684
+ if (header && !header.endsWith('\n\n')) {
2685
+ header += '\n'
2686
+ }
2567
2687
  dts = header + dts
2568
2688
  const dtsWithTypeImports = rewriteTypeImportReferences(
2569
2689
  header + dtsWithTypeImportMarkers,
@@ -28,6 +28,7 @@ import {
28
28
  pick,
29
29
  resolvePackageReconciliationPaths,
30
30
  restrictWasiNodeEngine,
31
+ serializeJson,
31
32
  wasiLoaderSuffix,
32
33
  wasiTargetHasThreads,
33
34
  withFileSystemReconciliation,
@@ -574,7 +575,7 @@ async function createNpmDirsUnlocked(
574
575
 
575
576
  const targetPackageJson = join(targetDir, 'package.json')
576
577
  pendingWrites.push({
577
- content: JSON.stringify(scopedPackageJson, null, 2) + '\n',
578
+ content: serializeJson(scopedPackageJson),
578
579
  destination: targetPackageJson,
579
580
  })
580
581
  if (wasmModuleTypeDef) {