@napi-rs/cli 3.8.6 → 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/dist/cli.js +9085 -8737
- package/dist/index.cjs +9092 -8744
- package/dist/index.d.cts +37 -17
- package/dist/index.d.ts +37 -17
- package/dist/index.js +9085 -8737
- package/docs/build.md +36 -34
- package/package.json +4 -5
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +9 -13
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/build.spec.ts +284 -0
- package/src/api/__tests__/create-npm-dirs.spec.ts +4 -3
- package/src/api/__tests__/templates.spec.ts +84 -1
- package/src/api/build.ts +181 -7
- package/src/api/create-npm-dirs.ts +2 -1
- package/src/api/new.ts +13 -18
- package/src/api/pre-publish.ts +5 -10
- package/src/api/rename.ts +10 -21
- package/src/api/templates/js-binding.ts +7 -9
- package/src/api/templates/load-wasi-template.ts +20 -17
- package/src/api/templates/wasi-worker-template.ts +36 -31
- package/src/commands/__tests__/build.spec.ts +24 -0
- package/src/commands/build.ts +1 -1
- package/src/def/build.ts +30 -4
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +18 -28
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
- package/src/utils/__tests__/misc.spec.ts +4 -0
- package/src/utils/__tests__/serialize.spec.ts +55 -0
- package/src/utils/__tests__/target.spec.ts +221 -0
- package/src/utils/__tests__/typegen.spec.ts +115 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +4 -5
- package/src/utils/serialize.ts +47 -0
- package/src/utils/target.ts +150 -1
- package/src/utils/typegen.ts +241 -30
|
@@ -1,13 +1,49 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs'
|
|
2
|
+
import { chmod, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
|
1
3
|
import os from 'os'
|
|
4
|
+
import { join } from 'node:path'
|
|
2
5
|
|
|
3
6
|
import test from 'ava'
|
|
4
7
|
|
|
5
8
|
import {
|
|
9
|
+
wasiLibcHasNewFutexAbi,
|
|
6
10
|
parseTriple,
|
|
7
11
|
getSystemDefaultTarget,
|
|
12
|
+
rustBundledWasiLibc,
|
|
13
|
+
wasiSdkMajorVersion,
|
|
8
14
|
AVAILABLE_TARGETS,
|
|
9
15
|
} from '../target.js'
|
|
10
16
|
|
|
17
|
+
async function withWasiSdkDir(
|
|
18
|
+
setup: (wasiSdkPath: string) => Promise<void>,
|
|
19
|
+
assertion: (wasiSdkPath: string) => void,
|
|
20
|
+
) {
|
|
21
|
+
const wasiSdkPath = await mkdtemp(join(os.tmpdir(), 'napi-rs-wasi-sdk-'))
|
|
22
|
+
try {
|
|
23
|
+
await setup(wasiSdkPath)
|
|
24
|
+
assertion(wasiSdkPath)
|
|
25
|
+
} finally {
|
|
26
|
+
await rm(wasiSdkPath, { recursive: true, force: true })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function writeVersionFile(wasiSdkPath: string, content: string) {
|
|
31
|
+
await writeFile(join(wasiSdkPath, 'VERSION'), content)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function writeVersionHeader(wasiSdkPath: string, content: string) {
|
|
35
|
+
const headerDir = join(
|
|
36
|
+
wasiSdkPath,
|
|
37
|
+
'share',
|
|
38
|
+
'wasi-sysroot',
|
|
39
|
+
'include',
|
|
40
|
+
'wasm32-wasip1-threads',
|
|
41
|
+
'wasi',
|
|
42
|
+
)
|
|
43
|
+
await mkdir(headerDir, { recursive: true })
|
|
44
|
+
await writeFile(join(headerDir, 'version.h'), content)
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
test('should parse triple correctly', (t) => {
|
|
12
48
|
t.snapshot(AVAILABLE_TARGETS.map(parseTriple))
|
|
13
49
|
})
|
|
@@ -17,3 +53,188 @@ test('should get system default target correctly', (t) => {
|
|
|
17
53
|
|
|
18
54
|
t.is(target.platform, os.platform())
|
|
19
55
|
})
|
|
56
|
+
|
|
57
|
+
test('should read the wasi-sdk major version from VERSION', async (t) => {
|
|
58
|
+
await withWasiSdkDir(
|
|
59
|
+
(wasiSdkPath) => writeVersionFile(wasiSdkPath, '34.0\n'),
|
|
60
|
+
(wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 34),
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('should ignore the build suffix in VERSION', async (t) => {
|
|
65
|
+
await withWasiSdkDir(
|
|
66
|
+
(wasiSdkPath) => writeVersionFile(wasiSdkPath, '33.0+m\n'),
|
|
67
|
+
(wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 33),
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('should read a pre wasi-sdk 30 VERSION', async (t) => {
|
|
72
|
+
await withWasiSdkDir(
|
|
73
|
+
(wasiSdkPath) => writeVersionFile(wasiSdkPath, '27.0\n'),
|
|
74
|
+
(wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 27),
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('should fall back to the wasi/version.h header', async (t) => {
|
|
79
|
+
await withWasiSdkDir(
|
|
80
|
+
(wasiSdkPath) =>
|
|
81
|
+
writeVersionHeader(
|
|
82
|
+
wasiSdkPath,
|
|
83
|
+
'#define __wasi_sdk_major__ 30\n#define __wasi_sdk_minor__ 0\n',
|
|
84
|
+
),
|
|
85
|
+
(wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 30),
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('should prefer VERSION over the wasi/version.h header', async (t) => {
|
|
90
|
+
await withWasiSdkDir(
|
|
91
|
+
async (wasiSdkPath) => {
|
|
92
|
+
await writeVersionFile(wasiSdkPath, '34.0\n')
|
|
93
|
+
await writeVersionHeader(wasiSdkPath, '#define __wasi_sdk_major__ 33\n')
|
|
94
|
+
},
|
|
95
|
+
(wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 34),
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('should return null when no version signal exists', async (t) => {
|
|
100
|
+
await withWasiSdkDir(
|
|
101
|
+
() => Promise.resolve(),
|
|
102
|
+
(wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), null),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('should return null instead of throwing on a malformed VERSION', async (t) => {
|
|
107
|
+
await withWasiSdkDir(
|
|
108
|
+
(wasiSdkPath) => writeVersionFile(wasiSdkPath, 'not a version at all\n'),
|
|
109
|
+
(wasiSdkPath) => {
|
|
110
|
+
t.notThrows(() => wasiSdkMajorVersion(wasiSdkPath))
|
|
111
|
+
t.is(wasiSdkMajorVersion(wasiSdkPath), null)
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('should return null when the wasi-sdk path does not exist', (t) => {
|
|
117
|
+
t.is(wasiSdkMajorVersion(join(os.tmpdir(), 'napi-rs-missing-wasi-sdk')), null)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('should detect the new futex ABI from a wasi-libc archive', async (t) => {
|
|
121
|
+
const dir = await mkdtemp(join(os.tmpdir(), 'napi-rs-wasi-libc-'))
|
|
122
|
+
try {
|
|
123
|
+
// wasi-libc moved the futex helpers into `futex.c` when it dropped the
|
|
124
|
+
// unused `int op` parameter. `__wait.c` survives either way, so only the
|
|
125
|
+
// presence of `futex.c` separates the two ABIs.
|
|
126
|
+
const newAbi = join(dir, 'libc-new.a')
|
|
127
|
+
const legacyAbi = join(dir, 'libc-legacy.a')
|
|
128
|
+
await writeFile(newAbi, '!<arch>\n__wait.c.obj\nfutex.c.obj\n')
|
|
129
|
+
await writeFile(
|
|
130
|
+
legacyAbi,
|
|
131
|
+
'!<arch>\n__wait.c.obj\n__wasilibc_busywait.c.obj\n',
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
t.true(wasiLibcHasNewFutexAbi(newAbi))
|
|
135
|
+
t.false(wasiLibcHasNewFutexAbi(legacyAbi))
|
|
136
|
+
} finally {
|
|
137
|
+
await rm(dir, { recursive: true, force: true })
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('should return null when the wasi-libc archive is unavailable', (t) => {
|
|
142
|
+
t.is(wasiLibcHasNewFutexAbi(null), null)
|
|
143
|
+
t.is(
|
|
144
|
+
wasiLibcHasNewFutexAbi(join(os.tmpdir(), 'napi-rs-missing-libc.a')),
|
|
145
|
+
null,
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// A fake `rustc` must be an executable file, and `execFileSync` cannot run a
|
|
150
|
+
// Windows `.cmd` shim without a shell. The behaviour under test does not
|
|
151
|
+
// depend on the platform, so cover it on POSIX only.
|
|
152
|
+
const posixOnly = process.platform === 'win32' ? test.serial.skip : test.serial
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Installs a `rustc` that reports its own working directory as the sysroot,
|
|
156
|
+
* so a test can tell which directory the probe ran in.
|
|
157
|
+
*/
|
|
158
|
+
async function withFakeRustc(
|
|
159
|
+
envVar: 'RUSTC' | 'CARGO_BUILD_RUSTC',
|
|
160
|
+
body: (dir: string) => Promise<void> | void,
|
|
161
|
+
) {
|
|
162
|
+
const dir = await mkdtemp(join(os.tmpdir(), 'napi-rs-fake-rustc-'))
|
|
163
|
+
const rustc = join(dir, 'rustc')
|
|
164
|
+
// `$PWD` comes from the inherited environment, so ask the kernel for the
|
|
165
|
+
// real working directory. `-P` also resolves the `/var` symlink on macOS.
|
|
166
|
+
await writeFile(rustc, '#!/bin/sh\necho "$(pwd -P)/sysroot"\n')
|
|
167
|
+
await chmod(rustc, 0o755)
|
|
168
|
+
const previous = process.env[envVar]
|
|
169
|
+
process.env[envVar] = rustc
|
|
170
|
+
try {
|
|
171
|
+
await body(dir)
|
|
172
|
+
} finally {
|
|
173
|
+
if (previous === undefined) {
|
|
174
|
+
delete process.env[envVar]
|
|
175
|
+
} else {
|
|
176
|
+
process.env[envVar] = previous
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
posixOnly('probes the Rust sysroot from the build cwd', async (t) => {
|
|
182
|
+
await withFakeRustc('RUSTC', async (dir) => {
|
|
183
|
+
const caller = join(dir, 'caller')
|
|
184
|
+
const project = join(dir, 'project')
|
|
185
|
+
await mkdir(caller)
|
|
186
|
+
await mkdir(project)
|
|
187
|
+
|
|
188
|
+
// `rustc` is a rustup shim, so the directory it runs in picks the
|
|
189
|
+
// toolchain. Cargo is spawned with the build cwd; the probe must match it.
|
|
190
|
+
t.is(
|
|
191
|
+
rustBundledWasiLibc('wasm32-wasip1-threads', project),
|
|
192
|
+
join(
|
|
193
|
+
realpathSync(project),
|
|
194
|
+
'sysroot',
|
|
195
|
+
'lib',
|
|
196
|
+
'rustlib',
|
|
197
|
+
'wasm32-wasip1-threads',
|
|
198
|
+
'lib',
|
|
199
|
+
'self-contained',
|
|
200
|
+
'libc.a',
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
t.not(
|
|
204
|
+
rustBundledWasiLibc('wasm32-wasip1-threads', caller),
|
|
205
|
+
rustBundledWasiLibc('wasm32-wasip1-threads', project),
|
|
206
|
+
)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
posixOnly('falls back to CARGO_BUILD_RUSTC when RUSTC is unset', async (t) => {
|
|
211
|
+
const previousRustc = process.env.RUSTC
|
|
212
|
+
delete process.env.RUSTC
|
|
213
|
+
try {
|
|
214
|
+
await withFakeRustc('CARGO_BUILD_RUSTC', (dir) => {
|
|
215
|
+
t.true(
|
|
216
|
+
rustBundledWasiLibc('wasm32-wasip1-threads', dir)?.startsWith(
|
|
217
|
+
realpathSync(dir),
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
} finally {
|
|
222
|
+
if (previousRustc !== undefined) {
|
|
223
|
+
process.env.RUSTC = previousRustc
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
posixOnly('returns null when the compiler cannot be run', (t) => {
|
|
229
|
+
const previous = process.env.RUSTC
|
|
230
|
+
process.env.RUSTC = join(os.tmpdir(), 'napi-rs-no-such-rustc')
|
|
231
|
+
try {
|
|
232
|
+
t.is(rustBundledWasiLibc('wasm32-wasip1-threads'), null)
|
|
233
|
+
} finally {
|
|
234
|
+
if (previous === undefined) {
|
|
235
|
+
delete process.env.RUSTC
|
|
236
|
+
} else {
|
|
237
|
+
process.env.RUSTC = previous
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
1
3
|
import { join } from 'path'
|
|
2
4
|
import { fileURLToPath } from 'url'
|
|
3
5
|
|
|
@@ -5,6 +7,24 @@ import test from 'ava'
|
|
|
5
7
|
|
|
6
8
|
import { correctStringIdent, processTypeDef } from '../typegen.js'
|
|
7
9
|
|
|
10
|
+
async function processInlineTypeDef(
|
|
11
|
+
lines: Array<Record<string, unknown>>,
|
|
12
|
+
constEnum = true,
|
|
13
|
+
runtimeStringEnum = false,
|
|
14
|
+
) {
|
|
15
|
+
const dir = await mkdtemp(join(tmpdir(), 'napi-typegen-'))
|
|
16
|
+
const file = join(dir, 'defs')
|
|
17
|
+
await writeFile(
|
|
18
|
+
file,
|
|
19
|
+
lines.map((line) => JSON.stringify(line)).join('\n') + '\n',
|
|
20
|
+
)
|
|
21
|
+
try {
|
|
22
|
+
return await processTypeDef(file, constEnum, runtimeStringEnum)
|
|
23
|
+
} finally {
|
|
24
|
+
await rm(dir, { recursive: true, force: true })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
test('should ident string correctly', (t) => {
|
|
9
29
|
const input = `
|
|
10
30
|
/**
|
|
@@ -83,3 +103,98 @@ test('runtimeStringEnum is a no-op when constEnum is set', async (t) => {
|
|
|
83
103
|
|
|
84
104
|
t.snapshot(dts)
|
|
85
105
|
})
|
|
106
|
+
|
|
107
|
+
test('simple type aliases stay on one line', async (t) => {
|
|
108
|
+
const { dts } = await processInlineTypeDef([
|
|
109
|
+
{ kind: 'type', name: 'CustomU32', def: 'number' },
|
|
110
|
+
])
|
|
111
|
+
t.true(dts.includes('export type CustomU32 = number'))
|
|
112
|
+
t.false(/export type CustomU32 =\s*\n/.test(dts))
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('empty classes do not contain a blank body', async (t) => {
|
|
116
|
+
const { dts } = await processInlineTypeDef([
|
|
117
|
+
{ kind: 'struct', name: 'Blake2BKey', def: '' },
|
|
118
|
+
])
|
|
119
|
+
t.true(dts.includes('export declare class Blake2BKey {}'))
|
|
120
|
+
t.false(dts.includes('class Blake2BKey {\n\n}'))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('no-const-enum string enums emit a spaced union', async (t) => {
|
|
124
|
+
const { dts } = await processTypeDef(flagFixture, false, false)
|
|
125
|
+
t.true(dts.includes("export type Status = 'Active' | 'Inactive'"))
|
|
126
|
+
t.false(dts.includes("'Active'|"))
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('no-const-enum string enums keep commas inside variant docs', async (t) => {
|
|
130
|
+
const { dts } = await processInlineTypeDef(
|
|
131
|
+
[
|
|
132
|
+
{
|
|
133
|
+
kind: 'string_enum',
|
|
134
|
+
name: 'Status',
|
|
135
|
+
def: "/** First, documented variant */\n Active = 'Active',\n Inactive = 'Inactive'",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
false,
|
|
139
|
+
false,
|
|
140
|
+
)
|
|
141
|
+
t.true(dts.includes("export type Status = 'Active' | 'Inactive'"))
|
|
142
|
+
t.false(dts.includes('/** First |'))
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('no-const-enum string enums keep Unicode variant names', async (t) => {
|
|
146
|
+
const { dts } = await processInlineTypeDef(
|
|
147
|
+
[
|
|
148
|
+
{
|
|
149
|
+
kind: 'string_enum',
|
|
150
|
+
name: 'Status',
|
|
151
|
+
def: "成功 = '成功',\n Failed = 'Failed'",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
false,
|
|
155
|
+
false,
|
|
156
|
+
)
|
|
157
|
+
t.true(dts.includes("export type Status = '成功' | 'Failed'"))
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('no-const-enum string enums keep quoted JS property names', async (t) => {
|
|
161
|
+
const { dts } = await processInlineTypeDef(
|
|
162
|
+
[
|
|
163
|
+
{
|
|
164
|
+
kind: 'string_enum',
|
|
165
|
+
name: 'Header',
|
|
166
|
+
def: "'content-type' = 'content-type',\n Plain = 'plain'",
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
false,
|
|
170
|
+
false,
|
|
171
|
+
)
|
|
172
|
+
t.true(dts.includes("export type Header = 'content-type' | 'plain'"))
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('no-const-enum string enums keep commas and escapes inside values', async (t) => {
|
|
176
|
+
const { dts } = await processInlineTypeDef(
|
|
177
|
+
[
|
|
178
|
+
{
|
|
179
|
+
kind: 'string_enum',
|
|
180
|
+
name: 'Label',
|
|
181
|
+
def: "Csv = 'a,b',\n Quoted = 'say \\'hi\\'',\n Count = 2",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
false,
|
|
185
|
+
false,
|
|
186
|
+
)
|
|
187
|
+
t.true(dts.includes("export type Label = 'a,b' | 'say \\'hi\\'' | 2"))
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('correctStringIdent keeps nested object-type fields indented', (t) => {
|
|
191
|
+
const input = `export declare function bufferComplexOverride(value: {
|
|
192
|
+
Buffer(): "line"
|
|
193
|
+
mapped: { [Buffer in keyof T]: T[Buffer] }
|
|
194
|
+
external: Buffer
|
|
195
|
+
}): void`
|
|
196
|
+
const formatted = correctStringIdent(input, 0)
|
|
197
|
+
t.true(formatted.includes(' mapped: { [Buffer in keyof T]: T[Buffer] }'))
|
|
198
|
+
t.true(formatted.includes(' external: Buffer'))
|
|
199
|
+
t.false(formatted.includes('\nmapped:'))
|
|
200
|
+
})
|
package/src/utils/index.ts
CHANGED
package/src/utils/misc.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { setTimeout as delay } from 'node:timers/promises'
|
|
|
44
44
|
|
|
45
45
|
import pkgJson from '../../package.json' with { type: 'json' }
|
|
46
46
|
import { debug } from './log.js'
|
|
47
|
+
import { serializeJson } from './serialize.js'
|
|
47
48
|
|
|
48
49
|
export const readFileAsync = readFile
|
|
49
50
|
export const writeFileAsync = writeFile
|
|
@@ -132,7 +133,8 @@ interface ReconciliationReclaimState {
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
type ReconciliationMetadataOwner =
|
|
135
|
-
|
|
136
|
+
| ReconciliationLockOwner
|
|
137
|
+
| ReconciliationReclaimOwner
|
|
136
138
|
|
|
137
139
|
interface ReconciliationCandidateState {
|
|
138
140
|
owner: ReconciliationMetadataOwner
|
|
@@ -6153,10 +6155,7 @@ export async function updatePackageJson(
|
|
|
6153
6155
|
return
|
|
6154
6156
|
}
|
|
6155
6157
|
const old = JSON.parse(await readFileAsync(path, 'utf8'))
|
|
6156
|
-
await writeFileAsync(
|
|
6157
|
-
path,
|
|
6158
|
-
JSON.stringify(mergePackageJson(old, partial), null, 2),
|
|
6159
|
-
)
|
|
6158
|
+
await writeFileAsync(path, serializeJson(mergePackageJson(old, partial)))
|
|
6160
6159
|
}
|
|
6161
6160
|
|
|
6162
6161
|
export const CLI_VERSION = pkgJson.version
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { stringify as stringifyToml } from '@std/toml'
|
|
2
|
+
import { dump as yamlDump } from 'js-yaml'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canonical JSON for files the CLI writes (package.json, napi config).
|
|
6
|
+
*
|
|
7
|
+
* Always 2-space indent and a POSIX trailing newline. Callers must not
|
|
8
|
+
* add another `\n` or omit one — every JSON writer uses this helper.
|
|
9
|
+
*/
|
|
10
|
+
export function serializeJson(value: unknown): string {
|
|
11
|
+
return `${JSON.stringify(value, null, 2)}\n`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Canonical TOML for files the CLI rewrites (Cargo.toml).
|
|
16
|
+
*
|
|
17
|
+
* `@std/toml` stringify inserts a leading blank line and drops comments.
|
|
18
|
+
* We cannot preserve comments through parse→stringify. We only strip the
|
|
19
|
+
* leading blank and keep exactly one trailing newline — no regex rewrite
|
|
20
|
+
* of array contents, which would split commas inside quoted strings.
|
|
21
|
+
*/
|
|
22
|
+
export function serializeToml(value: Record<string, unknown>): string {
|
|
23
|
+
let serialized = stringifyToml(value)
|
|
24
|
+
if (serialized.startsWith('\n')) {
|
|
25
|
+
serialized = serialized.slice(1)
|
|
26
|
+
}
|
|
27
|
+
if (!serialized.endsWith('\n')) {
|
|
28
|
+
serialized += '\n'
|
|
29
|
+
}
|
|
30
|
+
return serialized
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Canonical YAML for files the CLI rewrites (GitHub Actions workflows).
|
|
35
|
+
*
|
|
36
|
+
* Matches `napi new` / `napi rename` dump options and always ends with a
|
|
37
|
+
* trailing newline. Blank lines from the original document are not
|
|
38
|
+
* preserved — js-yaml dump cannot round-trip those.
|
|
39
|
+
*/
|
|
40
|
+
export function serializeYaml(value: unknown): string {
|
|
41
|
+
const serialized = yamlDump(value, {
|
|
42
|
+
lineWidth: -1,
|
|
43
|
+
noRefs: true,
|
|
44
|
+
sortKeys: false,
|
|
45
|
+
})
|
|
46
|
+
return serialized.endsWith('\n') ? serialized : `${serialized}\n`
|
|
47
|
+
}
|
package/src/utils/target.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
1
|
+
import { execFileSync, execSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
2
4
|
|
|
3
5
|
export type Platform = NodeJS.Platform | 'wasm' | 'wasi' | 'openharmony'
|
|
4
6
|
|
|
@@ -156,6 +158,153 @@ export function wasiTargetHasThreads(
|
|
|
156
158
|
return getWasiTarget(target)?.flavor === 'threads'
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
function readTextFileOrNull(path: string): string | null {
|
|
162
|
+
try {
|
|
163
|
+
return readFileSync(path, 'utf8')
|
|
164
|
+
} catch {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Detect the major version of a wasi-sdk installation.
|
|
171
|
+
*
|
|
172
|
+
* wasi-libc dropped the unused `int op` parameter from
|
|
173
|
+
* `__wasilibc_futex_wait_atomic_wait` and `__wasilibc_futex_wait_maybe_busy`,
|
|
174
|
+
* and wasi-sdk 34 is the first release that ships the 3-argument signature.
|
|
175
|
+
* Static archives compiled against the two signatures cannot be mixed, so the
|
|
176
|
+
* emnapi archives have to be picked by wasi-sdk version, not by target triple
|
|
177
|
+
* alone.
|
|
178
|
+
*
|
|
179
|
+
* `<wasiSdkPath>/VERSION` is the primary signal: every release ships it and
|
|
180
|
+
* its first line is the version (`27.0`, `33.0+m`, `34.0`, ...). The
|
|
181
|
+
* `wasi/version.h` header carrying `__wasi_sdk_major__` only appears from
|
|
182
|
+
* wasi-sdk 30 onwards, so it stays a fallback for trees without a `VERSION`.
|
|
183
|
+
*
|
|
184
|
+
* Returns `null` when neither signal is readable or parseable. Detection must
|
|
185
|
+
* never throw: an unknown wasi-sdk degrades to the legacy archives instead of
|
|
186
|
+
* failing the build.
|
|
187
|
+
*/
|
|
188
|
+
export function wasiSdkMajorVersion(wasiSdkPath: string): number | null {
|
|
189
|
+
const version = readTextFileOrNull(join(wasiSdkPath, 'VERSION'))
|
|
190
|
+
if (version) {
|
|
191
|
+
// `33.0+m` and friends carry a build suffix, so only the leading integer
|
|
192
|
+
// of the first line is meaningful.
|
|
193
|
+
const major = /^\s*(\d+)/.exec(version.split('\n', 1)[0])
|
|
194
|
+
if (major) {
|
|
195
|
+
return Number(major[1])
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const versionHeader = readTextFileOrNull(
|
|
199
|
+
join(
|
|
200
|
+
wasiSdkPath,
|
|
201
|
+
'share',
|
|
202
|
+
'wasi-sysroot',
|
|
203
|
+
'include',
|
|
204
|
+
'wasm32-wasip1-threads',
|
|
205
|
+
'wasi',
|
|
206
|
+
'version.h',
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
if (versionHeader) {
|
|
210
|
+
const major = /^\s*#\s*define\s+__wasi_sdk_major__\s+(\d+)/m.exec(
|
|
211
|
+
versionHeader,
|
|
212
|
+
)
|
|
213
|
+
if (major) {
|
|
214
|
+
return Number(major[1])
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Archive member that only exists in a wasi-libc carrying the 3-argument
|
|
222
|
+
* futex ABI.
|
|
223
|
+
*
|
|
224
|
+
* wasi-libc moved the wasi-threads futex helpers out of `__wait.c` and into a
|
|
225
|
+
* new `futex.c` in the same change that dropped the unused `int op` parameter
|
|
226
|
+
* (WebAssembly/wasi-libc#846). `__wait.c` still exists afterwards for other
|
|
227
|
+
* symbols, so the presence of `futex.c` — not the absence of `__wait.c` — is
|
|
228
|
+
* what separates the two ABIs.
|
|
229
|
+
*/
|
|
230
|
+
const NEW_FUTEX_ABI_ARCHIVE_MEMBER = 'futex.c.obj'
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Path to the wasi-libc that cargo links when no wasi-sdk is configured.
|
|
234
|
+
*
|
|
235
|
+
* Without `WASI_SDK_PATH` the target links through `rust-lld` against the
|
|
236
|
+
* wasi-libc bundled with the Rust standard library, so that copy — not a
|
|
237
|
+
* wasi-sdk — decides the futex ABI.
|
|
238
|
+
*
|
|
239
|
+
* `cwd` must be the directory Cargo is spawned in, because that is what
|
|
240
|
+
* selects the toolchain.
|
|
241
|
+
*
|
|
242
|
+
* Returns `null` when `rustc` cannot be queried or the target is not
|
|
243
|
+
* installed. Never throws: an undetectable sysroot degrades to the legacy
|
|
244
|
+
* archives.
|
|
245
|
+
*/
|
|
246
|
+
export function rustBundledWasiLibc(
|
|
247
|
+
wasiTarget: string,
|
|
248
|
+
cwd?: string,
|
|
249
|
+
): string | null {
|
|
250
|
+
let sysroot: string
|
|
251
|
+
try {
|
|
252
|
+
// `rustc` is a rustup shim: it resolves `rust-toolchain.toml` and any
|
|
253
|
+
// directory override from its working directory. Cargo is spawned with
|
|
254
|
+
// the build cwd, so the probe must use the same one or it can read a
|
|
255
|
+
// different toolchain's wasi-libc than the one that gets linked.
|
|
256
|
+
// `RUSTC` and `CARGO_BUILD_RUSTC` bypass the shim entirely; Cargo gives
|
|
257
|
+
// `RUSTC` precedence, so do the same. `build.rustc` from the project's
|
|
258
|
+
// `.cargo/config.toml` stays out of reach without invoking cargo itself.
|
|
259
|
+
sysroot = execFileSync(
|
|
260
|
+
process.env.RUSTC ?? process.env.CARGO_BUILD_RUSTC ?? 'rustc',
|
|
261
|
+
['--print', 'sysroot'],
|
|
262
|
+
{
|
|
263
|
+
cwd,
|
|
264
|
+
env: process.env,
|
|
265
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
.toString('utf8')
|
|
269
|
+
.trim()
|
|
270
|
+
} catch {
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
if (!sysroot) {
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
return join(
|
|
277
|
+
sysroot,
|
|
278
|
+
'lib',
|
|
279
|
+
'rustlib',
|
|
280
|
+
wasiTarget,
|
|
281
|
+
'lib',
|
|
282
|
+
'self-contained',
|
|
283
|
+
'libc.a',
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Whether a wasi-libc archive carries the 3-argument futex ABI.
|
|
289
|
+
*
|
|
290
|
+
* Returns `null` when the archive cannot be read, so callers can tell
|
|
291
|
+
* "definitely the legacy ABI" apart from "could not tell".
|
|
292
|
+
*/
|
|
293
|
+
export function wasiLibcHasNewFutexAbi(
|
|
294
|
+
libcArchivePath: string | null,
|
|
295
|
+
): boolean | null {
|
|
296
|
+
if (!libcArchivePath) {
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
let archive: Buffer
|
|
300
|
+
try {
|
|
301
|
+
archive = readFileSync(libcArchivePath)
|
|
302
|
+
} catch {
|
|
303
|
+
return null
|
|
304
|
+
}
|
|
305
|
+
return archive.includes(NEW_FUTEX_ABI_ARCHIVE_MEMBER, 0, 'latin1')
|
|
306
|
+
}
|
|
307
|
+
|
|
159
308
|
/**
|
|
160
309
|
* A triple is a specific format for specifying a target architecture.
|
|
161
310
|
* Triples may be referred to as a target triple which is the architecture for the artifact produced, and the host triple which is the architecture that the compiler is running on.
|