@napi-rs/cli 3.0.0-alpha.89 → 3.0.0-alpha.90

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/src/api/new.ts CHANGED
@@ -1,5 +1,10 @@
1
- import { execSync } from 'node:child_process'
1
+ import { exec, execSync } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { homedir } from 'node:os'
2
4
  import path from 'node:path'
5
+ import { promises as fs } from 'node:fs'
6
+
7
+ import { load as yamlLoad, dump as yamlDump } from 'js-yaml'
3
8
 
4
9
  import {
5
10
  applyDefaultNewOptions,
@@ -7,37 +12,283 @@ import {
7
12
  } from '../def/new.js'
8
13
  import {
9
14
  AVAILABLE_TARGETS,
10
- CLI_VERSION,
11
15
  debugFactory,
12
16
  DEFAULT_TARGETS,
13
17
  mkdirAsync,
14
18
  readdirAsync,
15
19
  statAsync,
16
- type SupportedTestFramework,
17
- writeFileAsync,
18
20
  SupportedPackageManager,
19
21
  } from '../utils/index.js'
20
22
  import { napiEngineRequirement } from '../utils/version.js'
23
+ import { renameProject } from './rename.js'
21
24
 
22
- import {
23
- createBuildRs,
24
- createCargoToml,
25
- createGithubActionsCIYml,
26
- createLibRs,
27
- createPackageJson,
28
- gitIgnore,
29
- npmIgnore,
30
- } from './templates/index.js'
31
- import { WasiTargetName } from './templates/ci-template.js'
25
+ // Template imports removed as we're now using external templates
32
26
 
33
27
  const debug = debugFactory('new')
34
28
 
35
- interface Output {
36
- target: string
37
- content: string
29
+ type NewOptions = Required<RawNewOptions>
30
+
31
+ const TEMPLATE_REPOS = {
32
+ yarn: 'https://github.com/napi-rs/package-template',
33
+ pnpm: 'https://github.com/napi-rs/package-template-pnpm',
34
+ } as const
35
+
36
+ async function checkGitCommand(): Promise<boolean> {
37
+ try {
38
+ await new Promise((resolve) => {
39
+ const cp = exec('git --version')
40
+ cp.on('error', () => {
41
+ resolve(false)
42
+ })
43
+ cp.on('exit', (code) => {
44
+ if (code === 0) {
45
+ resolve(true)
46
+ } else {
47
+ resolve(false)
48
+ }
49
+ })
50
+ })
51
+ return true
52
+ } catch {
53
+ return false
54
+ }
55
+ }
56
+
57
+ async function ensureCacheDir(
58
+ packageManager: SupportedPackageManager,
59
+ ): Promise<string> {
60
+ const cacheDir = path.join(homedir(), '.napi-rs', 'template', packageManager)
61
+ await mkdirAsync(cacheDir, { recursive: true })
62
+ return cacheDir
38
63
  }
39
64
 
40
- type NewOptions = Required<RawNewOptions>
65
+ async function downloadTemplate(
66
+ packageManager: SupportedPackageManager,
67
+ cacheDir: string,
68
+ ): Promise<void> {
69
+ const repoUrl = TEMPLATE_REPOS[packageManager]
70
+ const templatePath = path.join(cacheDir, 'repo')
71
+
72
+ if (existsSync(templatePath)) {
73
+ debug(`Template cache found at ${templatePath}, updating...`)
74
+ try {
75
+ // Fetch latest changes and reset to remote
76
+ await new Promise<void>((resolve, reject) => {
77
+ const cp = exec('git fetch origin', { cwd: templatePath })
78
+ cp.on('error', reject)
79
+ cp.on('exit', (code) => {
80
+ if (code === 0) {
81
+ resolve()
82
+ } else {
83
+ reject(
84
+ new Error(
85
+ `Failed to fetch latest changes, git process exited with code ${code}`,
86
+ ),
87
+ )
88
+ }
89
+ })
90
+ })
91
+ execSync('git reset --hard origin/main', {
92
+ cwd: templatePath,
93
+ stdio: 'ignore',
94
+ })
95
+ debug('Template updated successfully')
96
+ } catch (error) {
97
+ debug(`Failed to update template: ${error}`)
98
+ throw new Error(`Failed to update template from ${repoUrl}: ${error}`)
99
+ }
100
+ } else {
101
+ debug(`Cloning template from ${repoUrl}...`)
102
+ try {
103
+ execSync(`git clone ${repoUrl} repo`, { cwd: cacheDir, stdio: 'inherit' })
104
+ debug('Template cloned successfully')
105
+ } catch (error) {
106
+ throw new Error(`Failed to clone template from ${repoUrl}: ${error}`)
107
+ }
108
+ }
109
+ }
110
+
111
+ async function copyDirectory(
112
+ src: string,
113
+ dest: string,
114
+ includeWasiBindings: boolean,
115
+ ): Promise<void> {
116
+ await mkdirAsync(dest, { recursive: true })
117
+ const entries = await fs.readdir(src, { withFileTypes: true })
118
+
119
+ for (const entry of entries) {
120
+ const srcPath = path.join(src, entry.name)
121
+ const destPath = path.join(dest, entry.name)
122
+
123
+ // Skip .git directory
124
+ if (entry.name === '.git') {
125
+ continue
126
+ }
127
+
128
+ if (entry.isDirectory()) {
129
+ await copyDirectory(srcPath, destPath, includeWasiBindings)
130
+ } else {
131
+ if (
132
+ !includeWasiBindings &&
133
+ (entry.name.endsWith('.wasi-browser.js') ||
134
+ entry.name.endsWith('.wasi.cjs') ||
135
+ entry.name.endsWith('wasi-worker.browser.mjs ') ||
136
+ entry.name.endsWith('wasi-worker.mjs') ||
137
+ entry.name.endsWith('browser.js'))
138
+ ) {
139
+ continue
140
+ }
141
+ await fs.copyFile(srcPath, destPath)
142
+ }
143
+ }
144
+ }
145
+
146
+ async function filterTargetsInPackageJson(
147
+ filePath: string,
148
+ enabledTargets: string[],
149
+ ): Promise<void> {
150
+ const content = await fs.readFile(filePath, 'utf-8')
151
+ const packageJson = JSON.parse(content)
152
+
153
+ // Filter napi.targets
154
+ if (packageJson.napi?.targets) {
155
+ packageJson.napi.targets = packageJson.napi.targets.filter(
156
+ (target: string) => enabledTargets.includes(target),
157
+ )
158
+ }
159
+
160
+ await fs.writeFile(filePath, JSON.stringify(packageJson, null, 2) + '\n')
161
+ }
162
+
163
+ async function filterTargetsInGithubActions(
164
+ filePath: string,
165
+ enabledTargets: string[],
166
+ ): Promise<void> {
167
+ const content = await fs.readFile(filePath, 'utf-8')
168
+ const yaml = yamlLoad(content) as any
169
+
170
+ const macOSAndWindowsTargets = new Set([
171
+ 'x86_64-pc-windows-msvc',
172
+ 'aarch64-pc-windows-msvc',
173
+ 'x86_64-apple-darwin',
174
+ ])
175
+
176
+ const linuxTargets = new Set([
177
+ 'x86_64-unknown-linux-gnu',
178
+ 'x86_64-unknown-linux-musl',
179
+ 'aarch64-unknown-linux-gnu',
180
+ 'aarch64-unknown-linux-musl',
181
+ 'armv7-unknown-linux-gnueabihf',
182
+ 'armv7-unknown-linux-musleabihf',
183
+ 'riscv64gc-unknown-linux-gnu',
184
+ 'powerpc64le-unknown-linux-gnu',
185
+ 's390x-unknown-linux-gnu',
186
+ 'aarch64-linux-android',
187
+ 'armv7-linux-androideabi',
188
+ ])
189
+
190
+ // Check if any Linux targets are enabled
191
+ const hasLinuxTargets = enabledTargets.some((target) =>
192
+ linuxTargets.has(target),
193
+ )
194
+
195
+ // Filter the matrix configurations in the build job
196
+ if (yaml?.jobs?.build?.strategy?.matrix?.settings) {
197
+ yaml.jobs.build.strategy.matrix.settings =
198
+ yaml.jobs.build.strategy.matrix.settings.filter((setting: any) => {
199
+ if (setting.target) {
200
+ return enabledTargets.includes(setting.target)
201
+ }
202
+ return true
203
+ })
204
+ }
205
+
206
+ const jobsToRemove: string[] = []
207
+
208
+ if (enabledTargets.every((target) => !macOSAndWindowsTargets.has(target))) {
209
+ jobsToRemove.push('test-macOS-windows-binding')
210
+ } else {
211
+ // Filter the matrix configurations in the test-macOS-windows-binding job
212
+ if (
213
+ yaml?.jobs?.['test-macOS-windows-binding']?.strategy?.matrix?.settings
214
+ ) {
215
+ yaml.jobs['test-macOS-windows-binding'].strategy.matrix.settings =
216
+ yaml.jobs['test-macOS-windows-binding'].strategy.matrix.settings.filter(
217
+ (setting: any) => {
218
+ if (setting.target) {
219
+ return enabledTargets.includes(setting.target)
220
+ }
221
+ return true
222
+ },
223
+ )
224
+ }
225
+ }
226
+
227
+ // If no Linux targets are enabled, remove Linux-specific jobs
228
+ if (!hasLinuxTargets) {
229
+ // Remove test-linux-binding job
230
+ if (yaml?.jobs?.['test-linux-binding']) {
231
+ jobsToRemove.push('test-linux-binding')
232
+ }
233
+ } else {
234
+ // Filter the matrix configurations in the test-linux-x64-gnu-binding job
235
+ if (yaml?.jobs?.['test-linux-binding']?.strategy?.matrix?.target) {
236
+ yaml.jobs['test-linux-binding'].strategy.matrix.target = yaml.jobs[
237
+ 'test-linux-binding'
238
+ ].strategy.matrix.target.filter((target: string) => {
239
+ if (target) {
240
+ return enabledTargets.includes(target)
241
+ }
242
+ return true
243
+ })
244
+ }
245
+ }
246
+
247
+ if (!enabledTargets.includes('wasm32-wasip1-threads')) {
248
+ jobsToRemove.push('test-wasi')
249
+ }
250
+
251
+ if (!enabledTargets.includes('x86_64-unknown-freebsd')) {
252
+ jobsToRemove.push('build-freebsd')
253
+ }
254
+
255
+ // Filter other test jobs based on target
256
+ for (const [jobName, jobConfig] of Object.entries(yaml.jobs || {})) {
257
+ if (
258
+ jobName.startsWith('test-') &&
259
+ jobName !== 'test-macOS-windows-binding' &&
260
+ jobName !== 'test-linux-x64-gnu-binding'
261
+ ) {
262
+ // Extract target from job name or config
263
+ const job = jobConfig as any
264
+ if (job.strategy?.matrix?.settings?.[0]?.target) {
265
+ const target = job.strategy.matrix.settings[0].target
266
+ if (!enabledTargets.includes(target)) {
267
+ jobsToRemove.push(jobName)
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ // Remove jobs for disabled targets
274
+ for (const jobName of jobsToRemove) {
275
+ delete yaml.jobs[jobName]
276
+ }
277
+
278
+ if (Array.isArray(yaml.jobs?.publish?.needs)) {
279
+ yaml.jobs.publish.needs = yaml.jobs.publish.needs.filter(
280
+ (need: string) => !jobsToRemove.includes(need),
281
+ )
282
+ }
283
+
284
+ // Write back the filtered YAML
285
+ const updatedYaml = yamlDump(yaml, {
286
+ lineWidth: -1,
287
+ noRefs: true,
288
+ sortKeys: false,
289
+ })
290
+ await fs.writeFile(filePath, updatedYaml)
291
+ }
41
292
 
42
293
  function processOptions(options: RawNewOptions) {
43
294
  debug('Processing options...')
@@ -70,7 +321,7 @@ function processOptions(options: RawNewOptions) {
70
321
  encoding: 'utf8',
71
322
  })
72
323
  if (out.includes('wasm32-wasip1-threads')) {
73
- options.targets.map((target) =>
324
+ options.targets = options.targets.map((target) =>
74
325
  target === 'wasm32-wasi-preview1-threads'
75
326
  ? 'wasm32-wasip1-threads'
76
327
  : target,
@@ -90,11 +341,92 @@ export async function newProject(userOptions: RawNewOptions) {
90
341
  debug('Targets to be enabled:')
91
342
  debug(options.targets)
92
343
 
93
- const outputs = await generateFiles(options)
344
+ // Check if git is available
345
+ if (!(await checkGitCommand())) {
346
+ throw new Error(
347
+ 'Git is not installed or not available in PATH. Please install Git to continue.',
348
+ )
349
+ }
350
+
351
+ const packageManager = options.packageManager as SupportedPackageManager
94
352
 
353
+ // Ensure target directory exists and is empty
95
354
  await ensurePath(options.path, options.dryRun)
96
355
 
97
- await dumpOutputs(outputs, options.dryRun)
356
+ if (!options.dryRun) {
357
+ try {
358
+ // Download or update template
359
+ const cacheDir = await ensureCacheDir(packageManager)
360
+ await downloadTemplate(packageManager, cacheDir)
361
+
362
+ // Copy template files to target directory
363
+ const templatePath = path.join(cacheDir, 'repo')
364
+ await copyDirectory(
365
+ templatePath,
366
+ options.path,
367
+ options.targets.includes('wasm32-wasip1-threads'),
368
+ )
369
+
370
+ // Rename project using the rename API
371
+ await renameProject({
372
+ cwd: options.path,
373
+ name: options.name,
374
+ binaryName: getBinaryName(options.name),
375
+ })
376
+
377
+ // Filter targets in package.json
378
+ const packageJsonPath = path.join(options.path, 'package.json')
379
+ if (existsSync(packageJsonPath)) {
380
+ await filterTargetsInPackageJson(packageJsonPath, options.targets)
381
+ }
382
+
383
+ // Filter targets in GitHub Actions CI
384
+ const ciPath = path.join(options.path, '.github', 'workflows', 'CI.yml')
385
+ if (existsSync(ciPath) && options.enableGithubActions) {
386
+ await filterTargetsInGithubActions(ciPath, options.targets)
387
+ } else if (
388
+ !options.enableGithubActions &&
389
+ existsSync(path.join(options.path, '.github'))
390
+ ) {
391
+ // Remove .github directory if GitHub Actions is not enabled
392
+ await fs.rm(path.join(options.path, '.github'), {
393
+ recursive: true,
394
+ force: true,
395
+ })
396
+ }
397
+
398
+ // Update package.json with additional configurations
399
+ const pkgJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
400
+ const pkgJson = JSON.parse(pkgJsonContent)
401
+
402
+ // Update engine requirement
403
+ if (!pkgJson.engines) {
404
+ pkgJson.engines = {}
405
+ }
406
+ pkgJson.engines.node = napiEngineRequirement(options.minNodeApiVersion)
407
+
408
+ // Update license if different from template
409
+ if (options.license && pkgJson.license !== options.license) {
410
+ pkgJson.license = options.license
411
+ }
412
+
413
+ // Update test framework if needed
414
+ if (options.testFramework !== 'ava') {
415
+ // This would require more complex logic to update test scripts and dependencies
416
+ debug(
417
+ `Test framework ${options.testFramework} requested but not yet implemented`,
418
+ )
419
+ }
420
+
421
+ await fs.writeFile(
422
+ packageJsonPath,
423
+ JSON.stringify(pkgJson, null, 2) + '\n',
424
+ )
425
+ } catch (error) {
426
+ throw new Error(`Failed to create project: ${error}`)
427
+ }
428
+ }
429
+
98
430
  debug(`Project created at: ${options.path}`)
99
431
  }
100
432
 
@@ -131,129 +463,6 @@ async function ensurePath(path: string, dryRun = false) {
131
463
  }
132
464
  }
133
465
 
134
- async function generateFiles(options: NewOptions): Promise<Output[]> {
135
- const packageJson = await generatePackageJson(options)
136
- return [
137
- generateCargoToml,
138
- generateLibRs,
139
- generateBuildRs,
140
- generateGithubWorkflow,
141
- generateIgnoreFiles,
142
- ]
143
- .flatMap((generator) => {
144
- const output = generator(options)
145
-
146
- if (!output) {
147
- return []
148
- }
149
-
150
- if (Array.isArray(output)) {
151
- return output.map((o) => ({
152
- ...o,
153
- target: path.join(options.path, o.target),
154
- }))
155
- } else {
156
- return [{ ...output, target: path.join(options.path, output.target) }]
157
- }
158
- })
159
- .concat([
160
- { ...packageJson, target: path.join(options.path, packageJson.target) },
161
- ])
162
- }
163
-
164
- function generateCargoToml(options: NewOptions): Output {
165
- return {
166
- target: './Cargo.toml',
167
- content: createCargoToml({
168
- name: options.name,
169
- license: options.license,
170
- features: [`napi${options.minNodeApiVersion}`],
171
- deriveFeatures: options.enableTypeDef ? ['type-def'] : [],
172
- }),
173
- }
174
- }
175
-
176
- function generateLibRs(_options: NewOptions): Output {
177
- return {
178
- target: './src/lib.rs',
179
- content: createLibRs(),
180
- }
181
- }
182
-
183
- function generateBuildRs(_options: NewOptions): Output {
184
- return {
185
- target: './build.rs',
186
- content: createBuildRs(),
187
- }
188
- }
189
-
190
- async function generatePackageJson(options: NewOptions): Promise<Output> {
191
- return {
192
- target: './package.json',
193
- content: await createPackageJson({
194
- name: options.name,
195
- binaryName: getBinaryName(options.name),
196
- targets: options.targets,
197
- license: options.license,
198
- engineRequirement: napiEngineRequirement(options.minNodeApiVersion),
199
- cliVersion: CLI_VERSION,
200
- testFramework: options.testFramework as SupportedTestFramework,
201
- }),
202
- }
203
- }
204
-
205
- function generateGithubWorkflow(options: NewOptions): Output | null {
206
- if (!options.enableGithubActions) {
207
- return null
208
- }
209
-
210
- return {
211
- target: './.github/workflows/ci.yml',
212
- content: createGithubActionsCIYml(
213
- options.targets,
214
- options.packageManager as SupportedPackageManager,
215
- (options.targets.find((t) =>
216
- t.includes('wasm32-wasi'),
217
- ) as WasiTargetName) ?? 'wasm32-wasi-preview1-threads',
218
- ),
219
- }
220
- }
221
-
222
- function generateIgnoreFiles(_options: NewOptions): Output[] {
223
- return [
224
- {
225
- target: './.gitignore',
226
- content: gitIgnore,
227
- },
228
- {
229
- target: './.npmignore',
230
- content: npmIgnore,
231
- },
232
- ]
233
- }
234
-
235
- async function dumpOutputs(outputs: Output[], dryRun?: boolean) {
236
- for (const output of outputs) {
237
- if (!output) {
238
- continue
239
- }
240
-
241
- debug(`Writing project file: ${output.target}`)
242
- // only output content to logger instead of writing to file system
243
- if (dryRun) {
244
- debug(output.content)
245
- continue
246
- }
247
-
248
- try {
249
- await mkdirAsync(path.dirname(output.target), { recursive: true })
250
- await writeFileAsync(output.target, output.content, 'utf-8')
251
- } catch (e) {
252
- throw new Error(`Failed to write file: ${output.target}`, { cause: e })
253
- }
254
- }
255
- }
256
-
257
466
  function getBinaryName(name: string): string {
258
467
  return name.split('/').pop()!
259
468
  }
package/src/api/rename.ts CHANGED
@@ -1,14 +1,19 @@
1
- import { resolve } from 'node:path'
1
+ import { existsSync } from 'node:fs'
2
+ import { rename } from 'node:fs/promises'
3
+ import { resolve, join } from 'node:path'
2
4
 
5
+ import { parse as parseToml, stringify as stringifyToml } from '@std/toml'
6
+ import { load as yamlParse, dump as yamlStringify } from 'js-yaml'
3
7
  import { isNil, merge, omitBy, pick } from 'lodash-es'
8
+ import { findUp } from 'find-up'
4
9
 
5
10
  import { applyDefaultRenameOptions, RenameOptions } from '../def/rename.js'
6
- import { readFileAsync, writeFileAsync } from '../utils/index.js'
7
-
8
- import { createNpmDirs } from './create-npm-dirs.js'
11
+ import { readConfig, readFileAsync, writeFileAsync } from '../utils/index.js'
9
12
 
10
13
  export async function renameProject(userOptions: RenameOptions) {
11
14
  const options = applyDefaultRenameOptions(userOptions)
15
+ const napiConfig = await readConfig(options)
16
+ const oldName = napiConfig.binaryName
12
17
 
13
18
  const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
14
19
  const cargoTomlPath = resolve(options.cwd, options.manifestPath)
@@ -44,17 +49,89 @@ export async function renameProject(userOptions: RenameOptions) {
44
49
  JSON.stringify(packageJsonData, null, 2),
45
50
  )
46
51
 
47
- let tomlContent = await readFileAsync(cargoTomlPath, 'utf8')
48
- tomlContent = tomlContent.replace(
49
- /name\s?=\s?"([\w+])"/,
50
- `name = "${options.binaryName}"`,
51
- )
52
- await writeFileAsync(cargoTomlPath, tomlContent)
53
-
54
- await createNpmDirs({
55
- cwd: options.cwd,
56
- packageJsonPath: options.packageJsonPath,
57
- npmDir: options.npmDir,
58
- dryRun: false,
59
- })
52
+ const tomlContent = await readFileAsync(cargoTomlPath, 'utf8')
53
+ const cargoToml = parseToml(tomlContent) as any
54
+
55
+ // Update the package name
56
+ if (cargoToml.package && options.binaryName) {
57
+ // Sanitize the binary name for Rust package naming conventions
58
+ const sanitizedName = options.binaryName
59
+ .replace('@', '')
60
+ .replace('/', '_')
61
+ .replace(/-/g, '_')
62
+ .toLowerCase()
63
+ cargoToml.package.name = sanitizedName
64
+ }
65
+
66
+ // Stringify the updated TOML
67
+ const updatedTomlContent = stringifyToml(cargoToml)
68
+
69
+ await writeFileAsync(cargoTomlPath, updatedTomlContent)
70
+ if (oldName !== options.binaryName) {
71
+ const githubActionsPath = await findUp('.github', {
72
+ cwd: options.cwd,
73
+ type: 'directory',
74
+ })
75
+ if (githubActionsPath) {
76
+ const githubActionsCIYmlPath = join(
77
+ githubActionsPath,
78
+ 'workflows',
79
+ 'CI.yml',
80
+ )
81
+ if (existsSync(githubActionsCIYmlPath)) {
82
+ const githubActionsContent = await readFileAsync(
83
+ githubActionsCIYmlPath,
84
+ 'utf8',
85
+ )
86
+ const githubActionsData = yamlParse(githubActionsContent) as any
87
+ if (githubActionsData.env?.APP_NAME) {
88
+ githubActionsData.env.APP_NAME = options.binaryName
89
+ await writeFileAsync(
90
+ githubActionsCIYmlPath,
91
+ yamlStringify(githubActionsData, {
92
+ lineWidth: -1,
93
+ noRefs: true,
94
+ sortKeys: false,
95
+ }),
96
+ )
97
+ }
98
+ }
99
+ }
100
+ const oldWasiBrowserBindingPath = join(
101
+ options.cwd,
102
+ `${oldName}.wasi-browser.js`,
103
+ )
104
+ if (existsSync(oldWasiBrowserBindingPath)) {
105
+ await rename(
106
+ oldWasiBrowserBindingPath,
107
+ join(options.cwd, `${options.binaryName}.wasi-browser.js`),
108
+ )
109
+ }
110
+ const oldWasiBindingPath = join(options.cwd, `${oldName}.wasi.cjs`)
111
+ if (existsSync(oldWasiBindingPath)) {
112
+ await rename(
113
+ oldWasiBindingPath,
114
+ join(options.cwd, `${options.binaryName}.wasi.cjs`),
115
+ )
116
+ }
117
+ const gitAttributesPath = join(options.cwd, '.gitattributes')
118
+ if (existsSync(gitAttributesPath)) {
119
+ const gitAttributesContent = await readFileAsync(
120
+ gitAttributesPath,
121
+ 'utf8',
122
+ )
123
+ const gitAttributesData = gitAttributesContent
124
+ .split('\n')
125
+ .map((line) => {
126
+ return line
127
+ .replace(
128
+ `${oldName}.wasi-browser.js`,
129
+ `${options.binaryName}.wasi-browser.js`,
130
+ )
131
+ .replace(`${oldName}.wasi.cjs`, `${options.binaryName}.wasi.cjs`)
132
+ })
133
+ .join('\n')
134
+ await writeFileAsync(gitAttributesPath, gitAttributesData)
135
+ }
136
+ }
60
137
  }
@@ -6,7 +6,7 @@ import { DEFAULT_TARGETS, parseTriple, Target } from './target.js'
6
6
 
7
7
  export type ValueOfConstArray<T> = T[Exclude<keyof T, keyof Array<any>>]
8
8
 
9
- export const SupportedPackageManagers = ['yarn'] as const
9
+ export const SupportedPackageManagers = ['yarn', 'pnpm'] as const
10
10
  export const SupportedTestFrameworks = ['ava'] as const
11
11
 
12
12
  export type SupportedPackageManager = ValueOfConstArray<
@@ -6,3 +6,4 @@ export * from './metadata.js'
6
6
  export * from './config.js'
7
7
  export * from './cargo.js'
8
8
  export * from './typegen.js'
9
+ export * from './read-config.js'
@@ -0,0 +1,20 @@
1
+ import { resolve } from 'node:path'
2
+
3
+ import { readNapiConfig } from './config.js'
4
+
5
+ interface MinimalNapiOptions {
6
+ cwd: string
7
+ configPath?: string
8
+ packageJsonPath?: string
9
+ }
10
+
11
+ export async function readConfig(options: MinimalNapiOptions) {
12
+ const resolvePath = (...paths: string[]) => resolve(options.cwd, ...paths)
13
+ const config = await readNapiConfig(
14
+ resolvePath(
15
+ options.configPath ?? options.packageJsonPath ?? 'package.json',
16
+ ),
17
+ options.configPath ? resolvePath(options.configPath) : undefined,
18
+ )
19
+ return config
20
+ }