@napi-rs/cli 3.7.4 → 3.8.0
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 +9248 -1311
- package/dist/index.cjs +9268 -1331
- package/dist/index.d.cts +175 -157
- package/dist/index.d.ts +175 -157
- package/dist/index.js +9248 -1311
- 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 +14 -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 +472 -101
- 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
|
@@ -6,7 +6,13 @@ import { parentPort, Worker } from "node:worker_threads";
|
|
|
6
6
|
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
|
|
9
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
instantiateNapiModuleSync,
|
|
11
|
+
MessageHandler,
|
|
12
|
+
getDefaultContext,
|
|
13
|
+
emnapiAsyncWorkPlugin,
|
|
14
|
+
emnapiTSFNPlugin,
|
|
15
|
+
} = require("@napi-rs/wasm-runtime");
|
|
10
16
|
|
|
11
17
|
if (parentPort) {
|
|
12
18
|
parentPort.on("message", (data) => {
|
|
@@ -46,6 +52,11 @@ const handler = new MessageHandler({
|
|
|
46
52
|
childThread: true,
|
|
47
53
|
wasi,
|
|
48
54
|
context: emnapiContext,
|
|
55
|
+
// The wasm links a "basic" emnapi archive (no C async-work /
|
|
56
|
+
// threadsafe-function implementations), so every thread that
|
|
57
|
+
// instantiates it must provide the JavaScript implementations
|
|
58
|
+
// through the emnapi plugins.
|
|
59
|
+
plugins: [emnapiAsyncWorkPlugin, emnapiTSFNPlugin],
|
|
49
60
|
overwriteImports(importObject) {
|
|
50
61
|
importObject.env = {
|
|
51
62
|
...importObject.env,
|
|
@@ -68,11 +79,24 @@ export const createWasiBrowserWorkerBinding = (
|
|
|
68
79
|
errorEvent: boolean,
|
|
69
80
|
) => {
|
|
70
81
|
const fsImport = fs
|
|
71
|
-
? `import {
|
|
82
|
+
? `import {
|
|
83
|
+
instantiateNapiModuleSync,
|
|
84
|
+
MessageHandler,
|
|
85
|
+
WASI,
|
|
86
|
+
createFsProxy,
|
|
87
|
+
emnapiAsyncWorkPlugin,
|
|
88
|
+
emnapiTSFNPlugin,
|
|
89
|
+
} from '@napi-rs/wasm-runtime'
|
|
72
90
|
import { memfsExported as __memfsExported } from '@napi-rs/wasm-runtime/fs'
|
|
73
91
|
|
|
74
92
|
const fs = createFsProxy(__memfsExported)`
|
|
75
|
-
: `import {
|
|
93
|
+
: `import {
|
|
94
|
+
instantiateNapiModuleSync,
|
|
95
|
+
MessageHandler,
|
|
96
|
+
WASI,
|
|
97
|
+
emnapiAsyncWorkPlugin,
|
|
98
|
+
emnapiTSFNPlugin,
|
|
99
|
+
} from '@napi-rs/wasm-runtime'`
|
|
76
100
|
const errorOutputsAppend = errorEvent
|
|
77
101
|
? `\n errorOutputs.push([...arguments])`
|
|
78
102
|
: ''
|
|
@@ -118,6 +142,11 @@ const handler = new MessageHandler({
|
|
|
118
142
|
return instantiateNapiModuleSync(wasmModule, {
|
|
119
143
|
childThread: true,
|
|
120
144
|
wasi,
|
|
145
|
+
// The wasm links a "basic" emnapi archive (no C async-work /
|
|
146
|
+
// threadsafe-function implementations), so every thread that
|
|
147
|
+
// instantiates it must provide the JavaScript implementations
|
|
148
|
+
// through the emnapi plugins.
|
|
149
|
+
plugins: [emnapiAsyncWorkPlugin, emnapiTSFNPlugin],
|
|
121
150
|
overwriteImports(importObject) {
|
|
122
151
|
importObject.env = {
|
|
123
152
|
...importObject.env,
|
package/src/commands/rename.ts
CHANGED
|
@@ -6,14 +6,19 @@ import { BaseRenameCommand } from '../def/rename.js'
|
|
|
6
6
|
export class RenameCommand extends BaseRenameCommand {
|
|
7
7
|
async execute() {
|
|
8
8
|
const options = this.getOptions()
|
|
9
|
-
|
|
9
|
+
const hasExplicitRename = [
|
|
10
|
+
options.name,
|
|
11
|
+
options.binaryName,
|
|
12
|
+
options.packageName,
|
|
13
|
+
options.repository,
|
|
14
|
+
options.description,
|
|
15
|
+
].some((value) => value !== undefined)
|
|
16
|
+
if (!hasExplicitRename) {
|
|
10
17
|
const name = await input({
|
|
11
18
|
message: `Enter the new package name in the package.json`,
|
|
12
19
|
required: true,
|
|
13
20
|
})
|
|
14
21
|
options.name = name
|
|
15
|
-
}
|
|
16
|
-
if (!options.binaryName) {
|
|
17
22
|
const binaryName = await input({
|
|
18
23
|
message: `Enter the new binary name`,
|
|
19
24
|
required: true,
|
package/src/def/artifacts.ts
CHANGED
|
@@ -34,7 +34,7 @@ export abstract class BaseArtifactsCommand extends Command {
|
|
|
34
34
|
|
|
35
35
|
buildOutputDir?: string = Option.String('--build-output-dir', {
|
|
36
36
|
description:
|
|
37
|
-
'Path to the build output dir, only needed when targets
|
|
37
|
+
'Path to the build output dir, only needed when targets contain a WASI target',
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
getOptions() {
|
|
@@ -82,7 +82,7 @@ export interface ArtifactsOptions {
|
|
|
82
82
|
*/
|
|
83
83
|
npmDir?: string
|
|
84
84
|
/**
|
|
85
|
-
* Path to the build output dir, only needed when targets
|
|
85
|
+
* Path to the build output dir, only needed when targets contain a WASI target
|
|
86
86
|
*/
|
|
87
87
|
buildOutputDir?: string
|
|
88
88
|
}
|
|
@@ -167,8 +167,8 @@ Generated by [AVA](https://avajs.dev).
|
|
|
167
167
|
abi: 'wasi',
|
|
168
168
|
arch: 'wasm32',
|
|
169
169
|
platform: 'wasi',
|
|
170
|
-
platformArchABI: 'wasm32-
|
|
171
|
-
triple: 'wasm32-
|
|
170
|
+
platformArchABI: 'wasm32-wasip1',
|
|
171
|
+
triple: 'wasm32-wasip1',
|
|
172
172
|
},
|
|
173
173
|
{
|
|
174
174
|
abi: 'wasi',
|
|
Binary file
|
|
@@ -1,11 +1,29 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, type BigIntStats } from 'node:fs'
|
|
2
|
+
import {
|
|
3
|
+
lstat,
|
|
4
|
+
mkdtemp,
|
|
5
|
+
readdir,
|
|
6
|
+
readFile,
|
|
7
|
+
rename,
|
|
8
|
+
rm,
|
|
9
|
+
writeFile,
|
|
10
|
+
} from 'node:fs/promises'
|
|
3
11
|
import { tmpdir } from 'node:os'
|
|
4
12
|
import { join } from 'node:path'
|
|
5
13
|
|
|
6
14
|
import ava, { type TestFn } from 'ava'
|
|
7
15
|
|
|
8
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
retireFailedSnapshotLeftover,
|
|
18
|
+
snapshotLeftoverIsTransactionOwned,
|
|
19
|
+
statIdentitiesMatch,
|
|
20
|
+
updatePackageJson,
|
|
21
|
+
} from '../misc.js'
|
|
22
|
+
|
|
23
|
+
async function fileIdentityStrings(path: string) {
|
|
24
|
+
const stats = await lstat(path, { bigint: true })
|
|
25
|
+
return { dev: String(stats.dev), ino: String(stats.ino) }
|
|
26
|
+
}
|
|
9
27
|
|
|
10
28
|
const test = ava as TestFn<{
|
|
11
29
|
tmpDir: string
|
|
@@ -54,3 +72,106 @@ test('updatePackageJson merges nested objects instead of overwriting them', asyn
|
|
|
54
72
|
'@napi-rs/fixture-darwin-arm64': '1.0.1',
|
|
55
73
|
})
|
|
56
74
|
})
|
|
75
|
+
|
|
76
|
+
test('statIdentitiesMatch distinguishes inodes that collide as lossy Numbers', (t) => {
|
|
77
|
+
const dev = 1n
|
|
78
|
+
const ino = 2n ** 53n
|
|
79
|
+
const owned = { dev, ino }
|
|
80
|
+
const successor = { dev, ino: ino + 1n }
|
|
81
|
+
|
|
82
|
+
// The defect being guarded against: past Number.MAX_SAFE_INTEGER two
|
|
83
|
+
// distinct 64-bit identifiers collapse onto the same JS double, so a
|
|
84
|
+
// numeric Stats.dev/ino comparison cannot tell these files apart.
|
|
85
|
+
t.is(Number(owned.ino), Number(successor.ino))
|
|
86
|
+
|
|
87
|
+
t.false(statIdentitiesMatch(owned, successor))
|
|
88
|
+
t.false(statIdentitiesMatch({ dev: dev + 1n, ino }, owned))
|
|
89
|
+
t.true(statIdentitiesMatch(owned, { dev, ino }))
|
|
90
|
+
t.false(statIdentitiesMatch(owned, undefined))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('snapshotLeftoverIsTransactionOwned rejects Number-colliding successors', (t) => {
|
|
94
|
+
const identity = { dev: '1', ino: String(2n ** 53n) }
|
|
95
|
+
const ownedStats = {
|
|
96
|
+
isFile: () => true,
|
|
97
|
+
dev: 1n,
|
|
98
|
+
ino: 2n ** 53n,
|
|
99
|
+
} as unknown as BigIntStats
|
|
100
|
+
const successorStats = {
|
|
101
|
+
isFile: () => true,
|
|
102
|
+
dev: 1n,
|
|
103
|
+
ino: 2n ** 53n + 1n,
|
|
104
|
+
} as unknown as BigIntStats
|
|
105
|
+
|
|
106
|
+
t.true(snapshotLeftoverIsTransactionOwned(ownedStats, identity))
|
|
107
|
+
t.false(snapshotLeftoverIsTransactionOwned(successorStats, identity))
|
|
108
|
+
t.false(snapshotLeftoverIsTransactionOwned(undefined, identity))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('retireFailedSnapshotLeftover removes the transaction-owned inode', async (t) => {
|
|
112
|
+
const destination = join(t.context.tmpDir, 'leftover.tmp')
|
|
113
|
+
await writeFile(destination, 'partial snapshot')
|
|
114
|
+
const identity = await fileIdentityStrings(destination)
|
|
115
|
+
|
|
116
|
+
const result = await retireFailedSnapshotLeftover(destination, identity)
|
|
117
|
+
|
|
118
|
+
t.deepEqual(result, { outcome: 'removed' })
|
|
119
|
+
t.false(existsSync(destination))
|
|
120
|
+
t.deepEqual(await readdir(t.context.tmpDir), [])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('retireFailedSnapshotLeftover reports a missing leftover', async (t) => {
|
|
124
|
+
const destination = join(t.context.tmpDir, 'leftover.tmp')
|
|
125
|
+
await writeFile(destination, 'partial snapshot')
|
|
126
|
+
const identity = await fileIdentityStrings(destination)
|
|
127
|
+
await rm(destination)
|
|
128
|
+
|
|
129
|
+
const result = await retireFailedSnapshotLeftover(destination, identity)
|
|
130
|
+
|
|
131
|
+
t.deepEqual(result, { outcome: 'missing' })
|
|
132
|
+
t.deepEqual(await readdir(t.context.tmpDir), [])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('retireFailedSnapshotLeftover keeps a pre-existing non-owned successor', async (t) => {
|
|
136
|
+
const destination = join(t.context.tmpDir, 'leftover.tmp')
|
|
137
|
+
await writeFile(destination, 'partial snapshot')
|
|
138
|
+
const identity = await fileIdentityStrings(destination)
|
|
139
|
+
|
|
140
|
+
// Replace the owned inode with a distinct one before cleanup runs. The
|
|
141
|
+
// successor is created as a sibling first so it deterministically has a
|
|
142
|
+
// different inode, then atomically renamed over the destination.
|
|
143
|
+
const successor = join(t.context.tmpDir, 'successor.tmp')
|
|
144
|
+
await writeFile(successor, 'successor content')
|
|
145
|
+
await rename(successor, destination)
|
|
146
|
+
|
|
147
|
+
const result = await retireFailedSnapshotLeftover(destination, identity)
|
|
148
|
+
|
|
149
|
+
t.deepEqual(result, { outcome: 'kept' })
|
|
150
|
+
t.is(await readFile(destination, 'utf8'), 'successor content')
|
|
151
|
+
t.deepEqual(await readdir(t.context.tmpDir), ['leftover.tmp'])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('retireFailedSnapshotLeftover restores a successor swapped in during the race window', async (t) => {
|
|
155
|
+
const destination = join(t.context.tmpDir, 'leftover.tmp')
|
|
156
|
+
await writeFile(destination, 'partial snapshot')
|
|
157
|
+
const identity = await fileIdentityStrings(destination)
|
|
158
|
+
|
|
159
|
+
const successor = join(t.context.tmpDir, 'successor.tmp')
|
|
160
|
+
await writeFile(successor, 'successor content')
|
|
161
|
+
|
|
162
|
+
// Swap the successor onto the pathname after the ownership pre-check and
|
|
163
|
+
// before the retirement rename — the exact interval in which the previous
|
|
164
|
+
// lstat-then-unlink cleanup would have deleted a file the transaction never
|
|
165
|
+
// owned.
|
|
166
|
+
const result = await retireFailedSnapshotLeftover(
|
|
167
|
+
destination,
|
|
168
|
+
identity,
|
|
169
|
+
async () => {
|
|
170
|
+
await rename(successor, destination)
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
t.deepEqual(result, { outcome: 'kept' })
|
|
175
|
+
t.is(await readFile(destination, 'utf8'), 'successor content')
|
|
176
|
+
t.deepEqual(await readdir(t.context.tmpDir), ['leftover.tmp'])
|
|
177
|
+
})
|
|
@@ -1,7 +1,53 @@
|
|
|
1
1
|
import test from 'ava'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
MINIMUM_WASI_NODE_VERSION,
|
|
5
|
+
napiEngineRequirement,
|
|
6
|
+
restrictWasiNodeEngine,
|
|
7
|
+
SUPPORTED_NAPI_VERSIONS,
|
|
8
|
+
} from '../version.js'
|
|
4
9
|
|
|
5
10
|
test('should generate correct napi engine requirement', (t) => {
|
|
6
11
|
t.snapshot(SUPPORTED_NAPI_VERSIONS.map(napiEngineRequirement))
|
|
7
12
|
})
|
|
13
|
+
|
|
14
|
+
test('should keep node engine ranges inside the WASI floor', (t) => {
|
|
15
|
+
t.is(restrictWasiNodeEngine('^22.14.0'), '^22.14.0')
|
|
16
|
+
t.is(restrictWasiNodeEngine('>=24'), '>=24')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('should return the WASI floor for superset node engine ranges', (t) => {
|
|
20
|
+
t.is(restrictWasiNodeEngine('>=18'), MINIMUM_WASI_NODE_VERSION)
|
|
21
|
+
t.is(restrictWasiNodeEngine('>=12 <14 || >=18'), MINIMUM_WASI_NODE_VERSION)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('should preserve partial intersections with the WASI floor', (t) => {
|
|
25
|
+
t.is(restrictWasiNodeEngine('>=22'), '>=22.13.0 <23.0.0-0 || >=23.5.0')
|
|
26
|
+
t.is(restrictWasiNodeEngine('<21'), '>=20.19.0 <21.0.0-0')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('should reject node engine ranges disjoint from the WASI floor', (t) => {
|
|
30
|
+
for (const nodeRange of ['>=21 <22', '<20', '13.0.0', '21.0.0']) {
|
|
31
|
+
const error = t.throws(() => restrictWasiNodeEngine(nodeRange))
|
|
32
|
+
t.true(
|
|
33
|
+
error.message.includes(`"${nodeRange}"`),
|
|
34
|
+
`error should name the rejected range ${nodeRange}`,
|
|
35
|
+
)
|
|
36
|
+
t.true(
|
|
37
|
+
error.message.includes(`"${MINIMUM_WASI_NODE_VERSION}"`),
|
|
38
|
+
'error should name the supported WASI versions',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('should fall back to the WASI floor for malformed node engine ranges', (t) => {
|
|
44
|
+
t.is(restrictWasiNodeEngine('not-a-range'), MINIMUM_WASI_NODE_VERSION)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('napi engine requirements always intersect the WASI floor', (t) => {
|
|
48
|
+
for (const napiVersion of SUPPORTED_NAPI_VERSIONS) {
|
|
49
|
+
t.notThrows(() =>
|
|
50
|
+
restrictWasiNodeEngine(napiEngineRequirement(napiVersion)),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
})
|
package/src/utils/config.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface UserNapiConfig {
|
|
|
64
64
|
wasm?: {
|
|
65
65
|
/**
|
|
66
66
|
* https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Memory
|
|
67
|
-
* @default 4000 pages (
|
|
67
|
+
* @default 4000 pages (250 MiB), or 1024 pages (64 MiB) for the deferred workerd loader
|
|
68
68
|
*/
|
|
69
69
|
initialMemory?: number
|
|
70
70
|
/**
|
|
@@ -151,6 +151,7 @@ export interface CommonPackageJsonFields {
|
|
|
151
151
|
|
|
152
152
|
dependencies?: Record<string, string>
|
|
153
153
|
devDependencies?: Record<string, string>
|
|
154
|
+
optionalDependencies?: Record<string, string>
|
|
154
155
|
|
|
155
156
|
ava?: {
|
|
156
157
|
timeout?: string
|
|
@@ -265,7 +266,19 @@ export async function readNapiConfig(
|
|
|
265
266
|
throw new Error(`Duplicate targets are not allowed: ${duplicateTarget}`)
|
|
266
267
|
}
|
|
267
268
|
|
|
268
|
-
|
|
269
|
+
const parsedTargets = targets.map(parseTriple)
|
|
270
|
+
const outputTargets = new Map<string, string>()
|
|
271
|
+
for (const [index, target] of parsedTargets.entries()) {
|
|
272
|
+
const previous = outputTargets.get(target.platformArchABI)
|
|
273
|
+
if (previous) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Targets ${previous} and ${targets[index]} produce the same ${target.platformArchABI} artifact set. Choose one target spelling.`,
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
outputTargets.set(target.platformArchABI, targets[index])
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
napiConfig.targets = parsedTargets
|
|
269
282
|
|
|
270
283
|
return napiConfig
|
|
271
284
|
}
|
package/src/utils/metadata.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
3
4
|
|
|
4
5
|
import { debugFactory } from './log.js'
|
|
5
6
|
|
|
@@ -32,7 +33,7 @@ export interface Crate {
|
|
|
32
33
|
manifest_path: string
|
|
33
34
|
dependencies: Array<{
|
|
34
35
|
name: string
|
|
35
|
-
source: string
|
|
36
|
+
source: string | null
|
|
36
37
|
req: string
|
|
37
38
|
kind: string | null
|
|
38
39
|
rename: string | null
|
|
@@ -44,11 +45,16 @@ export interface Crate {
|
|
|
44
45
|
}>
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export interface
|
|
48
|
+
export interface CargoResolveNode {
|
|
48
49
|
id: string
|
|
50
|
+
dependencies: string[]
|
|
49
51
|
deps: Array<{
|
|
50
52
|
name: string
|
|
51
53
|
pkg: string
|
|
54
|
+
dep_kinds: Array<{
|
|
55
|
+
kind: string | null
|
|
56
|
+
target: string | null
|
|
57
|
+
}>
|
|
52
58
|
}>
|
|
53
59
|
features: string[]
|
|
54
60
|
}
|
|
@@ -59,24 +65,18 @@ export interface CargoWorkspaceMetadata {
|
|
|
59
65
|
workspace_members: string[]
|
|
60
66
|
target_directory: string
|
|
61
67
|
workspace_root: string
|
|
62
|
-
resolve
|
|
63
|
-
nodes: CrateResolveNode[]
|
|
68
|
+
resolve: {
|
|
64
69
|
root: string | null
|
|
70
|
+
nodes: CargoResolveNode[]
|
|
65
71
|
} | null
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
export interface CargoFeatureOptions {
|
|
69
|
-
features?: string[]
|
|
70
|
-
allFeatures?: boolean
|
|
71
|
-
noDefaultFeatures?: boolean
|
|
72
|
-
}
|
|
73
|
-
|
|
74
74
|
// Crates that will actually compile `napi-derive` in this build, and are therefore
|
|
75
75
|
// expected to emit a type-def intermediate file. Prefer the resolved dependency graph:
|
|
76
|
-
// a crate whose `napi-derive` dependency is optional and stays disabled
|
|
77
|
-
// type-def file, so force-building it would invalidate cargo's
|
|
78
|
-
// build without ever satisfying the type-def check. Fall back
|
|
79
|
-
// when the resolve graph is unavailable.
|
|
76
|
+
// a crate whose `napi-derive` dependency is optional and stays disabled in the current
|
|
77
|
+
// build never emits a type-def file, so force-building it would invalidate cargo's
|
|
78
|
+
// fingerprint on every build without ever satisfying the type-def check. Fall back
|
|
79
|
+
// to declared dependencies when the resolve graph is unavailable.
|
|
80
80
|
export function getNapiDeriveDependentCrates(
|
|
81
81
|
metadata: CargoWorkspaceMetadata,
|
|
82
82
|
): Crate[] {
|
|
@@ -99,56 +99,164 @@ export function getNapiDeriveDependentCrates(
|
|
|
99
99
|
return metadata.packages.filter((crate) => dependentPackageIds.has(crate.id))
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
interface ParseMetadataOptions {
|
|
103
|
+
allFeatures?: boolean
|
|
104
|
+
cargoOptions?: string[]
|
|
105
|
+
cwd?: string
|
|
106
|
+
featurePackage?: string
|
|
107
|
+
features?: string[]
|
|
108
|
+
filterPlatform?: string
|
|
109
|
+
noDefaultFeatures?: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createCargoMetadataInvocation(
|
|
103
113
|
manifestPath: string,
|
|
104
|
-
|
|
114
|
+
options: ParseMetadataOptions = {},
|
|
105
115
|
) {
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
const globalArgs: string[] = []
|
|
117
|
+
const metadataArgs = [
|
|
118
|
+
'metadata',
|
|
119
|
+
'--manifest-path',
|
|
120
|
+
manifestPath,
|
|
121
|
+
'--format-version',
|
|
122
|
+
'1',
|
|
123
|
+
]
|
|
124
|
+
const features = [
|
|
125
|
+
...new Set(
|
|
126
|
+
(options.features ?? []).flatMap((feature) =>
|
|
127
|
+
feature.split(/[,\s]+/).filter(Boolean),
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
]
|
|
131
|
+
let allFeatures = options.allFeatures === true
|
|
132
|
+
let noDefaultFeatures = options.noDefaultFeatures === true
|
|
133
|
+
const cargoOptions = options.cargoOptions ?? []
|
|
134
|
+
for (let index = 0; index < cargoOptions.length; index += 1) {
|
|
135
|
+
const option = cargoOptions[index]
|
|
136
|
+
if (option === '--config' || option === '-Z') {
|
|
137
|
+
const value = cargoOptions[index + 1]
|
|
138
|
+
if (value !== undefined) {
|
|
139
|
+
globalArgs.push(option, value)
|
|
140
|
+
index += 1
|
|
141
|
+
}
|
|
142
|
+
} else if (option.startsWith('--config=') || option.startsWith('-Z')) {
|
|
143
|
+
globalArgs.push(option)
|
|
144
|
+
} else if (option === '--features' || option === '-F') {
|
|
145
|
+
const value = cargoOptions[index + 1]
|
|
146
|
+
if (value !== undefined) {
|
|
147
|
+
features.push(...value.split(/[,\s]+/).filter(Boolean))
|
|
148
|
+
index += 1
|
|
149
|
+
}
|
|
150
|
+
} else if (option.startsWith('--features=') || option.startsWith('-F=')) {
|
|
151
|
+
features.push(
|
|
152
|
+
...option
|
|
153
|
+
.slice(option.indexOf('=') + 1)
|
|
154
|
+
.split(/[,\s]+/)
|
|
155
|
+
.filter(Boolean),
|
|
156
|
+
)
|
|
157
|
+
} else if (option.startsWith('-F') && option.length > 2) {
|
|
158
|
+
features.push(
|
|
159
|
+
...option
|
|
160
|
+
.slice(2)
|
|
161
|
+
.split(/[,\s]+/)
|
|
162
|
+
.filter(Boolean),
|
|
163
|
+
)
|
|
164
|
+
} else if (option === '--all-features') {
|
|
165
|
+
allFeatures = true
|
|
166
|
+
} else if (option === '--no-default-features') {
|
|
167
|
+
noDefaultFeatures = true
|
|
168
|
+
} else if (
|
|
169
|
+
option === '--locked' ||
|
|
170
|
+
option === '--offline' ||
|
|
171
|
+
option === '--frozen'
|
|
172
|
+
) {
|
|
173
|
+
metadataArgs.push(option)
|
|
174
|
+
}
|
|
108
175
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
176
|
+
const selectedFeatures = [
|
|
177
|
+
...new Set(
|
|
178
|
+
features.map((feature) =>
|
|
179
|
+
options.featurePackage && !feature.includes('/')
|
|
180
|
+
? `${options.featurePackage}/${feature}`
|
|
181
|
+
: feature,
|
|
182
|
+
),
|
|
183
|
+
),
|
|
184
|
+
].sort()
|
|
185
|
+
if (selectedFeatures.length > 0) {
|
|
186
|
+
metadataArgs.push('--features', selectedFeatures.join(','))
|
|
113
187
|
}
|
|
114
|
-
if (
|
|
115
|
-
|
|
188
|
+
if (allFeatures) {
|
|
189
|
+
metadataArgs.push('--all-features')
|
|
116
190
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
191
|
+
if (noDefaultFeatures) {
|
|
192
|
+
metadataArgs.push('--no-default-features')
|
|
193
|
+
}
|
|
194
|
+
if (options.filterPlatform) {
|
|
195
|
+
metadataArgs.push('--filter-platform', options.filterPlatform)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
// Deliberately NOT `process.env.CARGO ?? 'cargo'`: `$CARGO` only overrides
|
|
200
|
+
// the cargo/cross binary spawned for the actual build (see the Builder in
|
|
201
|
+
// api/build.ts) — tests poison `$CARGO` with a nonexistent path to assert
|
|
202
|
+
// no real build is spawned, and `cross metadata` is not a thing either.
|
|
203
|
+
// Metadata always comes from the host `cargo`.
|
|
204
|
+
command: 'cargo',
|
|
205
|
+
args: [...globalArgs, ...metadataArgs],
|
|
206
|
+
cwd: resolve(options.cwd ?? dirname(manifestPath)),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function parseMetadata(
|
|
211
|
+
manifestPath: string,
|
|
212
|
+
options: ParseMetadataOptions = {},
|
|
213
|
+
) {
|
|
214
|
+
if (!fs.existsSync(manifestPath)) {
|
|
215
|
+
throw new Error(`No crate found in manifest: ${manifestPath}`)
|
|
119
216
|
}
|
|
120
217
|
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
218
|
+
const invocation = createCargoMetadataInvocation(manifestPath, options)
|
|
219
|
+
// Resolving with the build's feature flags keeps `metadata.resolve` in sync
|
|
220
|
+
// with what `cargo build` will actually compile. Feature flags are rejected
|
|
221
|
+
// in some setups (e.g. a virtual workspace root manifest), so fall back to a
|
|
222
|
+
// plain invocation without them.
|
|
223
|
+
const featurelessArgs = stripFeatureArgs(invocation.args)
|
|
224
|
+
if (featurelessArgs.length !== invocation.args.length) {
|
|
125
225
|
try {
|
|
126
|
-
return await execCargoMetadata(
|
|
226
|
+
return await execCargoMetadata(invocation)
|
|
127
227
|
} catch (e) {
|
|
128
228
|
debug.warn(
|
|
129
229
|
`cargo metadata failed with feature flags, retrying without them: ${e}`,
|
|
130
230
|
)
|
|
131
231
|
}
|
|
232
|
+
return execCargoMetadata({ ...invocation, args: featurelessArgs })
|
|
132
233
|
}
|
|
234
|
+
return execCargoMetadata(invocation)
|
|
235
|
+
}
|
|
133
236
|
|
|
134
|
-
|
|
237
|
+
function stripFeatureArgs(args: string[]): string[] {
|
|
238
|
+
const stripped: string[] = []
|
|
239
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
240
|
+
const arg = args[index]
|
|
241
|
+
if (arg === '--features') {
|
|
242
|
+
// also skip the flag's value
|
|
243
|
+
index += 1
|
|
244
|
+
} else if (arg !== '--all-features' && arg !== '--no-default-features') {
|
|
245
|
+
stripped.push(arg)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return stripped
|
|
135
249
|
}
|
|
136
250
|
|
|
137
|
-
async function execCargoMetadata(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
...extraArgs,
|
|
147
|
-
],
|
|
148
|
-
{
|
|
149
|
-
stdio: 'pipe',
|
|
150
|
-
},
|
|
151
|
-
)
|
|
251
|
+
async function execCargoMetadata(invocation: {
|
|
252
|
+
command: string
|
|
253
|
+
args: string[]
|
|
254
|
+
cwd: string
|
|
255
|
+
}) {
|
|
256
|
+
const childProcess = spawn(invocation.command, invocation.args, {
|
|
257
|
+
cwd: invocation.cwd,
|
|
258
|
+
stdio: 'pipe',
|
|
259
|
+
})
|
|
152
260
|
|
|
153
261
|
let stdout = ''
|
|
154
262
|
let stderr = ''
|