@napi-rs/cli 3.7.2 → 3.7.3
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 +201 -52
- package/dist/index.cjs +200 -51
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +201 -52
- package/package.json +3 -3
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +353 -0
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/android-build.spec.ts +113 -0
- package/src/api/__tests__/artifacts.spec.ts +95 -0
- package/src/api/__tests__/build-regressions.spec.ts +128 -0
- package/src/api/__tests__/build.spec.ts +1020 -0
- package/src/api/__tests__/create-npm-dirs.spec.ts +561 -0
- package/src/api/__tests__/new-node-api-version.spec.ts +87 -0
- package/src/api/__tests__/new.spec.ts +527 -0
- package/src/api/__tests__/rename.spec.ts +181 -0
- package/src/api/__tests__/templates.spec.ts +164 -0
- package/src/api/__tests__/universalize.spec.ts +128 -0
- package/src/api/artifacts.ts +7 -16
- package/src/api/build.ts +346 -105
- package/src/api/new.ts +65 -1
- package/src/commands/__tests__/new.spec.ts +13 -0
- package/src/def/build.ts +6 -6
- package/src/utils/__tests__/__fixtures__/napi_type_def +202 -0
- package/src/utils/__tests__/__fixtures__/runtime_string_enum_flag +2 -0
- package/src/utils/__tests__/__snapshots__/config.spec.ts.md +97 -0
- package/src/utils/__tests__/__snapshots__/config.spec.ts.snap +0 -0
- package/src/utils/__tests__/__snapshots__/target.spec.ts.md +180 -0
- package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +1197 -0
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
- package/src/utils/__tests__/__snapshots__/version.spec.ts.md +22 -0
- package/src/utils/__tests__/__snapshots__/version.spec.ts.snap +0 -0
- package/src/utils/__tests__/config.spec.ts +81 -0
- package/src/utils/__tests__/metadata.spec.ts +28 -0
- package/src/utils/__tests__/misc.spec.ts +56 -0
- package/src/utils/__tests__/target.spec.ts +19 -0
- package/src/utils/__tests__/typegen.spec.ts +85 -0
- package/src/utils/__tests__/version.spec.ts +7 -0
- package/src/utils/version.ts +2 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile, rm } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
|
|
8
|
+
import { parse as parseToml } from '@std/toml'
|
|
9
|
+
import ava, { type TestFn } from 'ava'
|
|
10
|
+
import { load as yamlLoad } from 'js-yaml'
|
|
11
|
+
|
|
12
|
+
import { newProject } from '../new.js'
|
|
13
|
+
|
|
14
|
+
const test = ava as TestFn<{
|
|
15
|
+
tmpDir: string
|
|
16
|
+
}>
|
|
17
|
+
|
|
18
|
+
test.beforeEach(async (t) => {
|
|
19
|
+
// Create a unique temp directory for tests
|
|
20
|
+
const timestamp = Date.now()
|
|
21
|
+
const random = Math.random().toString(36).substring(7)
|
|
22
|
+
const tmpDir = join(
|
|
23
|
+
tmpdir(),
|
|
24
|
+
'napi-rs-test',
|
|
25
|
+
`new-project-${timestamp}-${random}`,
|
|
26
|
+
)
|
|
27
|
+
t.context = { tmpDir }
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test.afterEach.always(async (t) => {
|
|
31
|
+
// Clean up any created directories
|
|
32
|
+
if (existsSync(t.context.tmpDir)) {
|
|
33
|
+
await rm(t.context.tmpDir, { recursive: true, force: true })
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('create a new project with default options', async (t) => {
|
|
38
|
+
const projectPath = join(t.context.tmpDir, 'default-project')
|
|
39
|
+
|
|
40
|
+
await newProject({
|
|
41
|
+
path: projectPath,
|
|
42
|
+
enableDefaultTargets: true,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
t.true(existsSync(projectPath))
|
|
46
|
+
t.true(existsSync(join(projectPath, 'package.json')))
|
|
47
|
+
t.true(existsSync(join(projectPath, 'Cargo.toml')))
|
|
48
|
+
t.true(existsSync(join(projectPath, 'src')))
|
|
49
|
+
t.true(existsSync(join(projectPath, '.github', 'workflows', 'CI.yml')))
|
|
50
|
+
|
|
51
|
+
// Check package.json
|
|
52
|
+
const pkgJson = JSON.parse(
|
|
53
|
+
await readFile(join(projectPath, 'package.json'), 'utf-8'),
|
|
54
|
+
)
|
|
55
|
+
t.is(pkgJson.name, 'default-project')
|
|
56
|
+
t.is(pkgJson.napi.binaryName, 'default-project')
|
|
57
|
+
t.is(pkgJson.license, 'MIT')
|
|
58
|
+
t.truthy(pkgJson.engines.node)
|
|
59
|
+
t.false('browser' in pkgJson)
|
|
60
|
+
t.false(pkgJson.files.includes('browser.js'))
|
|
61
|
+
t.falsy(existsSync(join(projectPath, 'browser.js')))
|
|
62
|
+
t.falsy(existsSync(join(projectPath, 'default-project.wasi-browser.js')))
|
|
63
|
+
t.falsy(existsSync(join(projectPath, 'wasi-worker-browser.mjs')))
|
|
64
|
+
t.falsy(existsSync(join(projectPath, 'wasi-worker.mjs')))
|
|
65
|
+
const gitAttributes = await readFile(
|
|
66
|
+
join(projectPath, '.gitattributes'),
|
|
67
|
+
'utf-8',
|
|
68
|
+
)
|
|
69
|
+
t.truthy(gitAttributes.includes('default-project.wasi-browser.js'))
|
|
70
|
+
t.truthy(gitAttributes.includes('default-project.wasi.cjs'))
|
|
71
|
+
t.truthy(gitAttributes.includes('wasi-worker-browser.mjs'))
|
|
72
|
+
t.truthy(gitAttributes.includes('wasi-worker.mjs'))
|
|
73
|
+
const ciYaml = await readFile(
|
|
74
|
+
join(projectPath, '.github', 'workflows', 'CI.yml'),
|
|
75
|
+
'utf-8',
|
|
76
|
+
)
|
|
77
|
+
const yamlObject = yamlLoad(ciYaml) as any
|
|
78
|
+
t.is(yamlObject.env.APP_NAME, 'default-project')
|
|
79
|
+
t.falsy(yamlObject.jobs.publish.needs.includes('wasm32-wasip1-threads'))
|
|
80
|
+
t.falsy(
|
|
81
|
+
yamlObject.jobs['test-linux-binding'].strategy.matrix.target.includes(
|
|
82
|
+
'aarch64-unknown-linux-musl',
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('create a new project with custom name', async (t) => {
|
|
88
|
+
const projectPath = join(t.context.tmpDir, 'custom-name-dir')
|
|
89
|
+
|
|
90
|
+
await newProject({
|
|
91
|
+
path: projectPath,
|
|
92
|
+
name: '@my-scope/custom-package',
|
|
93
|
+
enableDefaultTargets: true,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
t.true(existsSync(projectPath))
|
|
97
|
+
|
|
98
|
+
const pkgJson = JSON.parse(
|
|
99
|
+
await readFile(join(projectPath, 'package.json'), 'utf-8'),
|
|
100
|
+
)
|
|
101
|
+
t.is(pkgJson.name, '@my-scope/custom-package')
|
|
102
|
+
t.is(pkgJson.napi.binaryName, 'custom-package')
|
|
103
|
+
|
|
104
|
+
const cargoToml = await readFile(join(projectPath, 'Cargo.toml'), 'utf-8')
|
|
105
|
+
// Verify that the package name was properly renamed to follow Rust naming conventions
|
|
106
|
+
t.true(cargoToml.includes('name = "custom_package"'))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('create a new project with custom path', async (t) => {
|
|
110
|
+
const customPath = join(t.context.tmpDir, 'nested', 'folders', 'my-project')
|
|
111
|
+
|
|
112
|
+
await newProject({
|
|
113
|
+
path: customPath,
|
|
114
|
+
enableDefaultTargets: true,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
t.true(existsSync(customPath))
|
|
118
|
+
t.true(existsSync(join(customPath, 'package.json')))
|
|
119
|
+
|
|
120
|
+
const pkgJson = JSON.parse(
|
|
121
|
+
await readFile(join(customPath, 'package.json'), 'utf-8'),
|
|
122
|
+
)
|
|
123
|
+
t.is(pkgJson.name, 'my-project')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('create a new project with custom path and name', async (t) => {
|
|
127
|
+
const projectPath = join(t.context.tmpDir, 'custom-dir')
|
|
128
|
+
|
|
129
|
+
await newProject({
|
|
130
|
+
path: projectPath,
|
|
131
|
+
name: 'custom-project-name',
|
|
132
|
+
enableDefaultTargets: true,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
t.true(existsSync(projectPath))
|
|
136
|
+
|
|
137
|
+
const pkgJson = JSON.parse(
|
|
138
|
+
await readFile(join(projectPath, 'package.json'), 'utf-8'),
|
|
139
|
+
)
|
|
140
|
+
t.is(pkgJson.name, 'custom-project-name')
|
|
141
|
+
t.is(pkgJson.napi.binaryName, 'custom-project-name')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('create a new project with custom path, name, and targets', async (t) => {
|
|
145
|
+
const projectPath = join(t.context.tmpDir, 'full-custom')
|
|
146
|
+
const customTargets = [
|
|
147
|
+
'x86_64-unknown-linux-gnu',
|
|
148
|
+
'x86_64-apple-darwin',
|
|
149
|
+
'aarch64-apple-darwin',
|
|
150
|
+
'wasm32-wasip1-threads',
|
|
151
|
+
'x86_64-unknown-freebsd',
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
await newProject({
|
|
155
|
+
path: projectPath,
|
|
156
|
+
name: '@custom/full-project',
|
|
157
|
+
targets: customTargets,
|
|
158
|
+
enableDefaultTargets: false,
|
|
159
|
+
license: 'Apache-2.0',
|
|
160
|
+
minNodeApiVersion: 6,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
t.true(existsSync(projectPath))
|
|
164
|
+
|
|
165
|
+
const pkgJson = JSON.parse(
|
|
166
|
+
await readFile(join(projectPath, 'package.json'), 'utf-8'),
|
|
167
|
+
)
|
|
168
|
+
t.is(pkgJson.name, '@custom/full-project')
|
|
169
|
+
t.is(pkgJson.napi.binaryName, 'full-project')
|
|
170
|
+
t.is(pkgJson.license, 'Apache-2.0')
|
|
171
|
+
t.true(pkgJson.engines.node.includes('>= 14.0.0'))
|
|
172
|
+
t.is(pkgJson.browser, 'browser.js')
|
|
173
|
+
t.true(pkgJson.files.includes('browser.js'))
|
|
174
|
+
t.true(existsSync(join(projectPath, 'browser.js')))
|
|
175
|
+
|
|
176
|
+
// Check that CI workflow only includes the specified targets
|
|
177
|
+
const ciYaml = await readFile(
|
|
178
|
+
join(projectPath, '.github', 'workflows', 'CI.yml'),
|
|
179
|
+
'utf-8',
|
|
180
|
+
)
|
|
181
|
+
const yamlObject = yamlLoad(ciYaml) as any
|
|
182
|
+
t.true(
|
|
183
|
+
yamlObject.jobs.build.strategy.matrix.settings.some(
|
|
184
|
+
(setting: any) => setting.target === 'x86_64-unknown-linux-gnu',
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
t.true(
|
|
188
|
+
yamlObject.jobs.build.strategy.matrix.settings.some(
|
|
189
|
+
(setting: any) => setting.target === 'x86_64-apple-darwin',
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
t.true(
|
|
193
|
+
yamlObject.jobs.build.strategy.matrix.settings.some(
|
|
194
|
+
(setting: any) => setting.target === 'aarch64-apple-darwin',
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
t.false(
|
|
198
|
+
yamlObject.jobs.build.strategy.matrix.settings.some(
|
|
199
|
+
(setting: any) => setting.target === 'x86_64-pc-windows-msvc',
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
t.true(
|
|
203
|
+
yamlObject.jobs.build.strategy.matrix.settings.some(
|
|
204
|
+
(setting: any) => setting.target === 'wasm32-wasip1-threads',
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
t.truthy(yamlObject.jobs['build-freebsd'])
|
|
208
|
+
t.truthy(yamlObject.jobs['test-wasi'])
|
|
209
|
+
t.falsy(
|
|
210
|
+
yamlObject.jobs['test-macOS-windows-binding'].strategy.matrix.settings.some(
|
|
211
|
+
(setting: any) => setting.target === 'x86_64-pc-windows-msvc',
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
t.truthy(
|
|
215
|
+
yamlObject.jobs['test-macOS-windows-binding'].strategy.matrix.settings.some(
|
|
216
|
+
(setting: any) => setting.target === 'aarch64-apple-darwin',
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('non Windows and macOS targets should remove test-macOS-windows-binding job', async (t) => {
|
|
222
|
+
const projectPath = join(t.context.tmpDir, 'no-windows-macos')
|
|
223
|
+
const targets = [
|
|
224
|
+
'x86_64-unknown-linux-gnu',
|
|
225
|
+
'aarch64-unknown-linux-gnu',
|
|
226
|
+
'wasm32-wasip1-threads',
|
|
227
|
+
'x86_64-unknown-freebsd',
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
await newProject({
|
|
231
|
+
path: projectPath,
|
|
232
|
+
targets,
|
|
233
|
+
enableDefaultTargets: false,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
t.true(existsSync(projectPath))
|
|
237
|
+
|
|
238
|
+
const ciYaml = await readFile(
|
|
239
|
+
join(projectPath, '.github', 'workflows', 'CI.yml'),
|
|
240
|
+
'utf-8',
|
|
241
|
+
)
|
|
242
|
+
const yamlObject = yamlLoad(ciYaml) as any
|
|
243
|
+
t.falsy(yamlObject.jobs['test-macOS-windows-binding'])
|
|
244
|
+
t.falsy(yamlObject.jobs.publish.needs.includes('test-macOS-windows-binding'))
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('aarch64-apple-darwin should keep test-macOS-windows-binding job', async (t) => {
|
|
248
|
+
const projectPath = join(t.context.tmpDir, 'apple-silicon-only')
|
|
249
|
+
const targets = ['aarch64-apple-darwin']
|
|
250
|
+
|
|
251
|
+
await newProject({
|
|
252
|
+
path: projectPath,
|
|
253
|
+
targets,
|
|
254
|
+
enableDefaultTargets: false,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
t.true(existsSync(projectPath))
|
|
258
|
+
|
|
259
|
+
const ciYaml = await readFile(
|
|
260
|
+
join(projectPath, '.github', 'workflows', 'CI.yml'),
|
|
261
|
+
'utf-8',
|
|
262
|
+
)
|
|
263
|
+
const yamlObject = yamlLoad(ciYaml) as any
|
|
264
|
+
t.truthy(yamlObject.jobs['test-macOS-windows-binding'])
|
|
265
|
+
t.deepEqual(
|
|
266
|
+
yamlObject.jobs['test-macOS-windows-binding'].strategy.matrix.settings.map(
|
|
267
|
+
(setting: any) => setting.target,
|
|
268
|
+
),
|
|
269
|
+
['aarch64-apple-darwin'],
|
|
270
|
+
)
|
|
271
|
+
t.truthy(yamlObject.jobs.publish.needs.includes('test-macOS-windows-binding'))
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('should remove test-linux-binding job if no Linux targets are enabled', async (t) => {
|
|
275
|
+
const projectPath = join(t.context.tmpDir, 'no-linux')
|
|
276
|
+
const targets = ['x86_64-apple-darwin', 'aarch64-apple-darwin']
|
|
277
|
+
|
|
278
|
+
await newProject({
|
|
279
|
+
path: projectPath,
|
|
280
|
+
targets,
|
|
281
|
+
enableDefaultTargets: false,
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
t.true(existsSync(projectPath))
|
|
285
|
+
|
|
286
|
+
const ciYaml = await readFile(
|
|
287
|
+
join(projectPath, '.github', 'workflows', 'CI.yml'),
|
|
288
|
+
'utf-8',
|
|
289
|
+
)
|
|
290
|
+
const yamlObject = yamlLoad(ciYaml) as any
|
|
291
|
+
t.falsy(yamlObject.jobs['test-linux-binding'])
|
|
292
|
+
t.falsy(yamlObject.jobs.publish.needs.includes('test-linux-binding'))
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('should fail when no path is provided', async (t) => {
|
|
296
|
+
await t.throwsAsync(
|
|
297
|
+
async () => {
|
|
298
|
+
await newProject({
|
|
299
|
+
enableDefaultTargets: true,
|
|
300
|
+
})
|
|
301
|
+
},
|
|
302
|
+
{ message: /Please provide the path as the argument/ },
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('should fail when path already exists and is not empty', async (t) => {
|
|
307
|
+
const projectPath = join(t.context.tmpDir, 'existing-project')
|
|
308
|
+
|
|
309
|
+
// Create directory with a file
|
|
310
|
+
await rm(projectPath, { recursive: true, force: true }).catch(() => {})
|
|
311
|
+
const { mkdirSync, writeFileSync } = await import('node:fs')
|
|
312
|
+
mkdirSync(projectPath, { recursive: true })
|
|
313
|
+
writeFileSync(join(projectPath, 'existing-file.txt'), 'content')
|
|
314
|
+
|
|
315
|
+
await t.throwsAsync(
|
|
316
|
+
async () => {
|
|
317
|
+
await newProject({
|
|
318
|
+
path: projectPath,
|
|
319
|
+
enableDefaultTargets: true,
|
|
320
|
+
})
|
|
321
|
+
},
|
|
322
|
+
{ message: /already exists and it's not empty/ },
|
|
323
|
+
)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('should fail when path is a file', async (t) => {
|
|
327
|
+
const filePath = join(t.context.tmpDir, 'file.txt')
|
|
328
|
+
|
|
329
|
+
// Create a file
|
|
330
|
+
const { writeFileSync } = await import('node:fs')
|
|
331
|
+
const { mkdirSync } = await import('node:fs')
|
|
332
|
+
mkdirSync(t.context.tmpDir, { recursive: true })
|
|
333
|
+
writeFileSync(filePath, 'content')
|
|
334
|
+
|
|
335
|
+
await t.throwsAsync(
|
|
336
|
+
async () => {
|
|
337
|
+
await newProject({
|
|
338
|
+
path: filePath,
|
|
339
|
+
enableDefaultTargets: true,
|
|
340
|
+
})
|
|
341
|
+
},
|
|
342
|
+
{ message: /already exists and it's not a directory/ },
|
|
343
|
+
)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('should fail when git is unavailable', async (t) => {
|
|
347
|
+
const projectPath = join(t.context.tmpDir, 'missing-git')
|
|
348
|
+
const moduleUrl = pathToFileURL(join(process.cwd(), 'src/api/new.ts')).href
|
|
349
|
+
|
|
350
|
+
const result = await new Promise<{
|
|
351
|
+
code: number | null
|
|
352
|
+
stderr: string
|
|
353
|
+
}>((resolve) => {
|
|
354
|
+
execFile(
|
|
355
|
+
process.execPath,
|
|
356
|
+
[
|
|
357
|
+
'--import',
|
|
358
|
+
'@oxc-node/core/register',
|
|
359
|
+
'--input-type=module',
|
|
360
|
+
'-e',
|
|
361
|
+
`const { newProject } = await import(${JSON.stringify(moduleUrl)}); await newProject({ path: ${JSON.stringify(projectPath)}, enableDefaultTargets: true });`,
|
|
362
|
+
],
|
|
363
|
+
{
|
|
364
|
+
cwd: process.cwd(),
|
|
365
|
+
env: {
|
|
366
|
+
...process.env,
|
|
367
|
+
PATH: '',
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
(error, _stdout, stderr) => {
|
|
371
|
+
resolve({
|
|
372
|
+
code:
|
|
373
|
+
error && 'code' in error && typeof error.code === 'number'
|
|
374
|
+
? error.code
|
|
375
|
+
: 0,
|
|
376
|
+
stderr,
|
|
377
|
+
})
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
t.not(result.code, 0)
|
|
383
|
+
t.regex(
|
|
384
|
+
result.stderr,
|
|
385
|
+
/Git is not installed or not available in PATH\. Please install Git to continue\./,
|
|
386
|
+
)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('dry run should not create any files', async (t) => {
|
|
390
|
+
const projectPath = join(t.context.tmpDir, 'dry-run-project')
|
|
391
|
+
|
|
392
|
+
await newProject({
|
|
393
|
+
path: projectPath,
|
|
394
|
+
name: 'dry-run-test',
|
|
395
|
+
enableDefaultTargets: true,
|
|
396
|
+
dryRun: true,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
t.false(existsSync(projectPath))
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('create project without GitHub Actions', async (t) => {
|
|
403
|
+
const projectPath = join(t.context.tmpDir, 'no-github-actions')
|
|
404
|
+
|
|
405
|
+
await newProject({
|
|
406
|
+
path: projectPath,
|
|
407
|
+
enableDefaultTargets: true,
|
|
408
|
+
enableGithubActions: false,
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
t.true(existsSync(projectPath))
|
|
412
|
+
t.true(existsSync(join(projectPath, 'package.json')))
|
|
413
|
+
t.false(existsSync(join(projectPath, '.github')))
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('create project without type-def', async (t) => {
|
|
417
|
+
const projectPath = join(t.context.tmpDir, 'no-type-def')
|
|
418
|
+
|
|
419
|
+
await newProject({
|
|
420
|
+
path: projectPath,
|
|
421
|
+
enableDefaultTargets: true,
|
|
422
|
+
enableTypeDef: false,
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const cargoToml = await readFile(join(projectPath, 'Cargo.toml'), 'utf-8')
|
|
426
|
+
const cargoTomlData = parseToml(cargoToml) as any
|
|
427
|
+
t.is(cargoTomlData.dependencies['napi-derive']['default-features'], false)
|
|
428
|
+
t.false(
|
|
429
|
+
cargoTomlData.dependencies['napi-derive'].features.includes('type-def'),
|
|
430
|
+
)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
test('create a new project with pnpm package manager', async (t) => {
|
|
434
|
+
const projectPath = join(t.context.tmpDir, 'pnpm-project')
|
|
435
|
+
|
|
436
|
+
await newProject({
|
|
437
|
+
path: projectPath,
|
|
438
|
+
name: 'pnpm-test-project',
|
|
439
|
+
packageManager: 'pnpm',
|
|
440
|
+
enableDefaultTargets: true,
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
t.true(existsSync(projectPath))
|
|
444
|
+
t.true(existsSync(join(projectPath, 'package.json')))
|
|
445
|
+
t.true(existsSync(join(projectPath, 'Cargo.toml')))
|
|
446
|
+
|
|
447
|
+
// Check package.json
|
|
448
|
+
const pkgJson = JSON.parse(
|
|
449
|
+
await readFile(join(projectPath, 'package.json'), 'utf-8'),
|
|
450
|
+
)
|
|
451
|
+
t.is(pkgJson.name, 'pnpm-test-project')
|
|
452
|
+
t.is(pkgJson.napi.binaryName, 'pnpm-test-project')
|
|
453
|
+
|
|
454
|
+
// Verify that the Cargo.toml has the correct sanitized name
|
|
455
|
+
const cargoToml = await readFile(join(projectPath, 'Cargo.toml'), 'utf-8')
|
|
456
|
+
t.true(cargoToml.includes('name = "pnpm_test_project"'))
|
|
457
|
+
|
|
458
|
+
// Check for pnpm-specific files or configurations if any
|
|
459
|
+
// The template might have different structure for pnpm
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
test('create a new project with pnpm and custom name', async (t) => {
|
|
463
|
+
const projectPath = join(t.context.tmpDir, 'pnpm-custom-name')
|
|
464
|
+
|
|
465
|
+
await newProject({
|
|
466
|
+
path: projectPath,
|
|
467
|
+
name: '@my-org/custom-pnpm-package',
|
|
468
|
+
packageManager: 'pnpm',
|
|
469
|
+
enableDefaultTargets: true,
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
t.true(existsSync(projectPath))
|
|
473
|
+
t.true(existsSync(join(projectPath, 'package.json')))
|
|
474
|
+
|
|
475
|
+
const pkgJson = JSON.parse(
|
|
476
|
+
await readFile(join(projectPath, 'package.json'), 'utf-8'),
|
|
477
|
+
)
|
|
478
|
+
t.is(pkgJson.name, '@my-org/custom-pnpm-package')
|
|
479
|
+
t.is(pkgJson.napi.binaryName, 'custom-pnpm-package')
|
|
480
|
+
|
|
481
|
+
const cargoToml = await readFile(join(projectPath, 'Cargo.toml'), 'utf-8')
|
|
482
|
+
t.true(cargoToml.includes('name = "custom_pnpm_package"'))
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
test('create a new project with pnpm and custom path/name combination', async (t) => {
|
|
486
|
+
const projectPath = join(t.context.tmpDir, 'deep', 'nested', 'pnpm-dir')
|
|
487
|
+
|
|
488
|
+
await newProject({
|
|
489
|
+
path: projectPath,
|
|
490
|
+
name: '@scoped/pnpm-custom-name',
|
|
491
|
+
packageManager: 'pnpm',
|
|
492
|
+
enableDefaultTargets: true,
|
|
493
|
+
license: 'Apache-2.0',
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
t.true(existsSync(projectPath))
|
|
497
|
+
t.true(existsSync(join(projectPath, 'package.json')))
|
|
498
|
+
t.true(existsSync(join(projectPath, 'Cargo.toml')))
|
|
499
|
+
|
|
500
|
+
// Check package.json
|
|
501
|
+
const pkgJson = JSON.parse(
|
|
502
|
+
await readFile(join(projectPath, 'package.json'), 'utf-8'),
|
|
503
|
+
)
|
|
504
|
+
t.is(pkgJson.name, '@scoped/pnpm-custom-name')
|
|
505
|
+
t.is(pkgJson.napi.binaryName, 'pnpm-custom-name')
|
|
506
|
+
t.is(pkgJson.license, 'Apache-2.0')
|
|
507
|
+
|
|
508
|
+
// Check Cargo.toml has sanitized name
|
|
509
|
+
const cargoToml = await readFile(join(projectPath, 'Cargo.toml'), 'utf-8')
|
|
510
|
+
t.true(cargoToml.includes('name = "pnpm_custom_name"'))
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
test('should fail when no targets are enabled', async (t) => {
|
|
514
|
+
const projectPath = join(t.context.tmpDir, 'no-targets')
|
|
515
|
+
|
|
516
|
+
await t.throwsAsync(
|
|
517
|
+
async () => {
|
|
518
|
+
await newProject({
|
|
519
|
+
path: projectPath,
|
|
520
|
+
enableDefaultTargets: false,
|
|
521
|
+
enableAllTargets: false,
|
|
522
|
+
targets: [],
|
|
523
|
+
})
|
|
524
|
+
},
|
|
525
|
+
{ message: /At least one target must be enabled/ },
|
|
526
|
+
)
|
|
527
|
+
})
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import ava, { type TestFn } from 'ava'
|
|
7
|
+
import { load as yamlLoad } from 'js-yaml'
|
|
8
|
+
|
|
9
|
+
import { renameProject } from '../rename.js'
|
|
10
|
+
|
|
11
|
+
const test = ava as TestFn<{
|
|
12
|
+
tmpDir: string
|
|
13
|
+
}>
|
|
14
|
+
|
|
15
|
+
test.beforeEach((t) => {
|
|
16
|
+
const timestamp = Date.now()
|
|
17
|
+
const random = Math.random().toString(36).substring(7)
|
|
18
|
+
t.context = {
|
|
19
|
+
tmpDir: join(
|
|
20
|
+
tmpdir(),
|
|
21
|
+
'napi-rs-test',
|
|
22
|
+
`rename-project-${timestamp}-${random}`,
|
|
23
|
+
),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test.afterEach.always(async (t) => {
|
|
28
|
+
if (existsSync(t.context.tmpDir)) {
|
|
29
|
+
await rm(t.context.tmpDir, { recursive: true, force: true })
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
async function createFixtureProject(
|
|
34
|
+
cwd: string,
|
|
35
|
+
options: {
|
|
36
|
+
packageJson: Record<string, unknown>
|
|
37
|
+
cargoPackageName: string
|
|
38
|
+
configPath?: string
|
|
39
|
+
configData?: Record<string, unknown>
|
|
40
|
+
},
|
|
41
|
+
) {
|
|
42
|
+
await mkdir(join(cwd, '.github', 'workflows'), { recursive: true })
|
|
43
|
+
|
|
44
|
+
await writeFile(
|
|
45
|
+
join(cwd, 'package.json'),
|
|
46
|
+
`${JSON.stringify(options.packageJson, null, 2)}\n`,
|
|
47
|
+
)
|
|
48
|
+
await writeFile(
|
|
49
|
+
join(cwd, 'Cargo.toml'),
|
|
50
|
+
`[package]\nname = "${options.cargoPackageName}"\n`,
|
|
51
|
+
)
|
|
52
|
+
await writeFile(
|
|
53
|
+
join(cwd, '.github', 'workflows', 'CI.yml'),
|
|
54
|
+
'env:\n APP_NAME: foo\njobs:\n build:\n runs-on: ubuntu-latest\n',
|
|
55
|
+
)
|
|
56
|
+
await writeFile(
|
|
57
|
+
join(cwd, '.gitattributes'),
|
|
58
|
+
'foo.wasi-browser.js linguist-generated=true\nfoo.wasi.cjs linguist-generated=true\n',
|
|
59
|
+
)
|
|
60
|
+
await writeFile(join(cwd, 'foo.wasi-browser.js'), 'browser binding\n')
|
|
61
|
+
await writeFile(join(cwd, 'foo.wasi.cjs'), 'node binding\n')
|
|
62
|
+
|
|
63
|
+
if (options.configPath && options.configData) {
|
|
64
|
+
await writeFile(
|
|
65
|
+
join(cwd, options.configPath),
|
|
66
|
+
`${JSON.stringify(options.configData, null, 2)}\n`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test('omitting binaryName keeps existing wasi artifact names and binary references', async (t) => {
|
|
72
|
+
const projectPath = join(t.context.tmpDir, 'artifact-rename')
|
|
73
|
+
|
|
74
|
+
await createFixtureProject(projectPath, {
|
|
75
|
+
packageJson: {
|
|
76
|
+
name: 'original',
|
|
77
|
+
napi: {
|
|
78
|
+
binaryName: 'foo',
|
|
79
|
+
packageName: '@scope/original',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
cargoPackageName: 'foo',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await renameProject({
|
|
86
|
+
cwd: projectPath,
|
|
87
|
+
name: 'renamed',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const packageJson = JSON.parse(
|
|
91
|
+
await readFile(join(projectPath, 'package.json'), 'utf8'),
|
|
92
|
+
)
|
|
93
|
+
const cargoToml = await readFile(join(projectPath, 'Cargo.toml'), 'utf8')
|
|
94
|
+
const gitAttributes = await readFile(
|
|
95
|
+
join(projectPath, '.gitattributes'),
|
|
96
|
+
'utf8',
|
|
97
|
+
)
|
|
98
|
+
const ciYaml = yamlLoad(
|
|
99
|
+
await readFile(join(projectPath, '.github', 'workflows', 'CI.yml'), 'utf8'),
|
|
100
|
+
) as any
|
|
101
|
+
|
|
102
|
+
t.is(packageJson.name, 'renamed')
|
|
103
|
+
t.is(packageJson.napi.binaryName, 'foo')
|
|
104
|
+
t.is(packageJson.napi.packageName, '@scope/original')
|
|
105
|
+
t.true(cargoToml.includes('name = "foo"'))
|
|
106
|
+
t.true(existsSync(join(projectPath, 'foo.wasi-browser.js')))
|
|
107
|
+
t.true(existsSync(join(projectPath, 'foo.wasi.cjs')))
|
|
108
|
+
t.false(existsSync(join(projectPath, 'undefined.wasi-browser.js')))
|
|
109
|
+
t.false(existsSync(join(projectPath, 'undefined.wasi.cjs')))
|
|
110
|
+
t.true(gitAttributes.includes('foo.wasi-browser.js'))
|
|
111
|
+
t.true(gitAttributes.includes('foo.wasi.cjs'))
|
|
112
|
+
t.false(gitAttributes.includes('undefined.wasi-browser.js'))
|
|
113
|
+
t.false(gitAttributes.includes('undefined.wasi.cjs'))
|
|
114
|
+
t.is(ciYaml.env.APP_NAME, 'foo')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('omitting binaryName preserves separated napi config fields', async (t) => {
|
|
118
|
+
const projectPath = join(t.context.tmpDir, 'config-rename')
|
|
119
|
+
|
|
120
|
+
await createFixtureProject(projectPath, {
|
|
121
|
+
packageJson: {
|
|
122
|
+
name: 'original',
|
|
123
|
+
},
|
|
124
|
+
cargoPackageName: 'foo',
|
|
125
|
+
configPath: 'napi.json',
|
|
126
|
+
configData: {
|
|
127
|
+
binaryName: 'foo',
|
|
128
|
+
packageName: '@scope/original',
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await renameProject({
|
|
133
|
+
cwd: projectPath,
|
|
134
|
+
configPath: 'napi.json',
|
|
135
|
+
name: 'renamed',
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const config = JSON.parse(
|
|
139
|
+
await readFile(join(projectPath, 'napi.json'), 'utf8'),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
t.is(config.binaryName, 'foo')
|
|
143
|
+
t.is(config.packageName, '@scope/original')
|
|
144
|
+
t.true(existsSync(join(projectPath, 'foo.wasi-browser.js')))
|
|
145
|
+
t.true(existsSync(join(projectPath, 'foo.wasi.cjs')))
|
|
146
|
+
t.false(existsSync(join(projectPath, 'undefined.wasi-browser.js')))
|
|
147
|
+
t.false(existsSync(join(projectPath, 'undefined.wasi.cjs')))
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('repository updates package.json when provided', async (t) => {
|
|
151
|
+
const projectPath = join(t.context.tmpDir, 'repository-rename')
|
|
152
|
+
|
|
153
|
+
await createFixtureProject(projectPath, {
|
|
154
|
+
packageJson: {
|
|
155
|
+
name: 'original',
|
|
156
|
+
repository: {
|
|
157
|
+
type: 'git',
|
|
158
|
+
url: 'https://example.com/old.git',
|
|
159
|
+
},
|
|
160
|
+
napi: {
|
|
161
|
+
binaryName: 'foo',
|
|
162
|
+
packageName: '@scope/original',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
cargoPackageName: 'foo',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await renameProject({
|
|
169
|
+
cwd: projectPath,
|
|
170
|
+
name: 'renamed',
|
|
171
|
+
repository: 'https://example.com/new.git',
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const packageJson = JSON.parse(
|
|
175
|
+
await readFile(join(projectPath, 'package.json'), 'utf8'),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
t.is(packageJson.name, 'renamed')
|
|
179
|
+
t.is(packageJson.repository.url, 'https://example.com/new.git')
|
|
180
|
+
t.is(packageJson.repository.type, 'git')
|
|
181
|
+
})
|