@napi-rs/cli 3.0.0-alpha.9 → 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.
Files changed (163) hide show
  1. package/cli.mjs +1 -1
  2. package/dist/api/__tests__/new.spec.d.ts +1 -0
  3. package/dist/api/__tests__/new.spec.js +283 -0
  4. package/dist/api/__tests__/new.spec.js.map +1 -0
  5. package/dist/api/artifacts.js +28 -7
  6. package/dist/api/artifacts.js.map +1 -1
  7. package/dist/api/build.d.ts +1 -1
  8. package/dist/api/build.js +324 -115
  9. package/dist/api/build.js.map +1 -1
  10. package/dist/api/create-npm-dirs.js +46 -9
  11. package/dist/api/create-npm-dirs.js.map +1 -1
  12. package/dist/api/new.js +312 -107
  13. package/dist/api/new.js.map +1 -1
  14. package/dist/api/pre-publish.js +9 -7
  15. package/dist/api/pre-publish.js.map +1 -1
  16. package/dist/api/rename.js +74 -12
  17. package/dist/api/rename.js.map +1 -1
  18. package/dist/api/templates/cargo.toml.js +1 -0
  19. package/dist/api/templates/cargo.toml.js.map +1 -1
  20. package/dist/api/templates/ci-template.d.ts +3 -1
  21. package/dist/api/templates/ci-template.js +140 -125
  22. package/dist/api/templates/ci-template.js.map +1 -1
  23. package/dist/api/templates/ci.yml.d.ts +3 -1
  24. package/dist/api/templates/ci.yml.js +21 -5
  25. package/dist/api/templates/ci.yml.js.map +1 -1
  26. package/dist/api/templates/js-binding.d.ts +1 -0
  27. package/dist/api/templates/js-binding.js +142 -222
  28. package/dist/api/templates/js-binding.js.map +1 -1
  29. package/dist/api/templates/load-wasi-template.d.ts +2 -0
  30. package/dist/api/templates/load-wasi-template.js +168 -0
  31. package/dist/api/templates/load-wasi-template.js.map +1 -0
  32. package/dist/api/templates/package.json.d.ts +9 -2
  33. package/dist/api/templates/package.json.js +26 -4
  34. package/dist/api/templates/package.json.js.map +1 -1
  35. package/dist/api/templates/wasi-worker-template.d.ts +2 -0
  36. package/dist/api/templates/wasi-worker-template.js +122 -0
  37. package/dist/api/templates/wasi-worker-template.js.map +1 -0
  38. package/dist/api/universalize.js +5 -5
  39. package/dist/api/universalize.js.map +1 -1
  40. package/dist/api/version.js +2 -2
  41. package/dist/api/version.js.map +1 -1
  42. package/dist/commands/artifacts.js +2 -2
  43. package/dist/commands/artifacts.js.map +1 -1
  44. package/dist/commands/build.js +1 -1
  45. package/dist/commands/build.js.map +1 -1
  46. package/dist/commands/new.js +34 -34
  47. package/dist/commands/new.js.map +1 -1
  48. package/dist/def/artifacts.d.ts +21 -1
  49. package/dist/def/artifacts.js +10 -2
  50. package/dist/def/artifacts.js.map +1 -1
  51. package/dist/def/build.d.ts +149 -4
  52. package/dist/def/build.js +26 -4
  53. package/dist/def/build.js.map +1 -1
  54. package/dist/def/create-npm-dirs.d.ts +10 -0
  55. package/dist/def/create-npm-dirs.js +4 -0
  56. package/dist/def/create-npm-dirs.js.map +1 -1
  57. package/dist/def/new.d.ts +25 -7
  58. package/dist/def/new.js +11 -1
  59. package/dist/def/new.js.map +1 -1
  60. package/dist/def/pre-publish.d.ts +12 -2
  61. package/dist/def/pre-publish.js +7 -3
  62. package/dist/def/pre-publish.js.map +1 -1
  63. package/dist/def/rename.d.ts +16 -6
  64. package/dist/def/rename.js +5 -1
  65. package/dist/def/rename.js.map +1 -1
  66. package/dist/def/universalize.d.ts +10 -0
  67. package/dist/def/universalize.js +4 -0
  68. package/dist/def/universalize.js.map +1 -1
  69. package/dist/def/version.d.ts +10 -0
  70. package/dist/def/version.js +4 -0
  71. package/dist/def/version.js.map +1 -1
  72. package/dist/index.cjs +8743 -14849
  73. package/dist/utils/__tests__/config.spec.d.ts +1 -0
  74. package/dist/utils/__tests__/config.spec.js +63 -0
  75. package/dist/utils/__tests__/config.spec.js.map +1 -0
  76. package/dist/utils/cargo.js +2 -2
  77. package/dist/utils/cargo.js.map +1 -1
  78. package/dist/utils/config.d.ts +48 -4
  79. package/dist/utils/config.js +36 -4
  80. package/dist/utils/config.js.map +1 -1
  81. package/dist/utils/index.d.ts +1 -1
  82. package/dist/utils/index.js +1 -1
  83. package/dist/utils/index.js.map +1 -1
  84. package/dist/utils/log.js +1 -1
  85. package/dist/utils/log.js.map +1 -1
  86. package/dist/utils/metadata.d.ts +13 -1
  87. package/dist/utils/metadata.js +31 -11
  88. package/dist/utils/metadata.js.map +1 -1
  89. package/dist/utils/misc.d.ts +3 -3
  90. package/dist/utils/misc.js +13 -11
  91. package/dist/utils/misc.js.map +1 -1
  92. package/dist/utils/read-config.d.ts +7 -0
  93. package/dist/utils/read-config.js +8 -0
  94. package/dist/utils/read-config.js.map +1 -0
  95. package/dist/utils/target.d.ts +7 -5
  96. package/dist/utils/target.js +17 -3
  97. package/dist/utils/target.js.map +1 -1
  98. package/dist/utils/typegen.d.ts +1 -1
  99. package/dist/utils/typegen.js +63 -32
  100. package/dist/utils/typegen.js.map +1 -1
  101. package/dist/utils/version.d.ts +2 -1
  102. package/dist/utils/version.js +10 -8
  103. package/dist/utils/version.js.map +1 -1
  104. package/package.json +34 -32
  105. package/src/api/__tests__/new.spec.ts +429 -0
  106. package/src/api/artifacts.ts +81 -9
  107. package/src/api/build.ts +498 -149
  108. package/src/api/create-npm-dirs.ts +57 -9
  109. package/src/api/new.ts +389 -122
  110. package/src/api/pre-publish.ts +13 -7
  111. package/src/api/rename.ts +103 -17
  112. package/src/api/templates/cargo.toml.ts +1 -0
  113. package/src/api/templates/ci-template.ts +150 -125
  114. package/src/api/templates/ci.yml.ts +27 -6
  115. package/src/api/templates/js-binding.ts +148 -220
  116. package/src/api/templates/load-wasi-template.ts +182 -0
  117. package/src/api/templates/package.json.ts +47 -5
  118. package/src/api/templates/wasi-worker-template.ts +122 -0
  119. package/src/api/universalize.ts +13 -8
  120. package/src/api/version.ts +5 -2
  121. package/src/commands/artifacts.ts +2 -2
  122. package/src/commands/build.ts +1 -1
  123. package/src/commands/new.ts +46 -45
  124. package/src/def/artifacts.ts +22 -3
  125. package/src/def/build.ts +55 -8
  126. package/src/def/create-npm-dirs.ts +9 -0
  127. package/src/def/new.ts +28 -3
  128. package/src/def/pre-publish.ts +12 -3
  129. package/src/def/rename.ts +11 -2
  130. package/src/def/universalize.ts +9 -0
  131. package/src/def/version.ts +9 -0
  132. package/src/utils/__tests__/__fixtures__/napi_type_def +130 -130
  133. package/src/utils/__tests__/__snapshots__/config.spec.ts.md +97 -0
  134. package/src/utils/__tests__/__snapshots__/config.spec.ts.snap +0 -0
  135. package/src/utils/__tests__/__snapshots__/target.spec.ts.md +28 -0
  136. package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
  137. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +316 -332
  138. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
  139. package/src/utils/__tests__/__snapshots__/version.spec.ts.md +8 -7
  140. package/src/utils/__tests__/__snapshots__/version.spec.ts.snap +0 -0
  141. package/src/utils/__tests__/config.spec.ts +81 -0
  142. package/src/utils/cargo.ts +2 -2
  143. package/src/utils/config.ts +110 -9
  144. package/src/utils/index.ts +1 -1
  145. package/src/utils/log.ts +1 -1
  146. package/src/utils/metadata.ts +52 -14
  147. package/src/utils/misc.ts +21 -12
  148. package/src/utils/read-config.ts +20 -0
  149. package/src/utils/target.ts +22 -4
  150. package/src/utils/typegen.ts +87 -52
  151. package/src/utils/version.ts +10 -8
  152. package/dist/api/load-wasi-template.d.ts +0 -1
  153. package/dist/api/load-wasi-template.js +0 -64
  154. package/dist/api/load-wasi-template.js.map +0 -1
  155. package/dist/api/wasi-worker-template.d.ts +0 -1
  156. package/dist/api/wasi-worker-template.js +0 -53
  157. package/dist/api/wasi-worker-template.js.map +0 -1
  158. package/dist/utils/ci.d.ts +0 -11
  159. package/dist/utils/ci.js +0 -96
  160. package/dist/utils/ci.js.map +0 -1
  161. package/src/api/load-wasi-template.ts +0 -66
  162. package/src/api/wasi-worker-template.ts +0 -52
  163. package/src/utils/ci.ts +0 -111
@@ -1,4 +1,6 @@
1
- import { join, resolve } from 'path'
1
+ import { join, resolve } from 'node:path'
2
+
3
+ import { parse } from 'semver'
2
4
 
3
5
  import {
4
6
  applyDefaultCreateNpmDirsOptions,
@@ -13,6 +15,8 @@ import {
13
15
  Target,
14
16
  } from '../utils/index.js'
15
17
 
18
+ import type { PackageMeta } from './templates/package.json.js'
19
+
16
20
  const debug = debugFactory('create-npm-dirs')
17
21
 
18
22
  export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
@@ -43,20 +47,25 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
43
47
  const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
44
48
  const npmPath = resolve(options.cwd, options.npmDir)
45
49
 
46
- debug(`Read content from [${packageJsonPath}]`)
50
+ debug(`Read content from [${options.configPath ?? packageJsonPath}]`)
47
51
 
48
52
  const { targets, binaryName, packageName, packageJson } =
49
- await readNapiConfig(packageJsonPath)
53
+ await readNapiConfig(
54
+ packageJsonPath,
55
+ options.configPath ? resolve(options.cwd, options.configPath) : undefined,
56
+ )
50
57
 
51
58
  for (const target of targets) {
52
59
  const targetDir = join(npmPath, `${target.platformArchABI}`)
53
60
  await mkdirAsync(targetDir)
54
61
 
55
- const binaryFileName = `${binaryName}.${target.platformArchABI}.node`
62
+ const binaryFileName =
63
+ target.arch === 'wasm32'
64
+ ? `${binaryName}.${target.platformArchABI}.wasm`
65
+ : `${binaryName}.${target.platformArchABI}.node`
56
66
  const scopedPackageJson = {
57
67
  name: `${packageName}-${target.platformArchABI}`,
58
68
  version: packageJson.version,
59
- os: [target.platform],
60
69
  cpu: target.arch !== 'universal' ? [target.arch] : undefined,
61
70
  main: binaryFileName,
62
71
  files: [binaryFileName],
@@ -74,9 +83,48 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
74
83
  'bugs',
75
84
  ),
76
85
  }
86
+ if (target.arch !== 'wasm32') {
87
+ // @ts-expect-error
88
+ scopedPackageJson.os = [target.platform]
89
+ } else {
90
+ const entry = `${binaryName}.wasi.cjs`
91
+ scopedPackageJson.main = entry
92
+ // @ts-expect-error
93
+ scopedPackageJson.browser = `${binaryName}.wasi-browser.js`
94
+ scopedPackageJson.files.push(
95
+ entry,
96
+ // @ts-expect-error
97
+ scopedPackageJson.browser,
98
+ `wasi-worker.mjs`,
99
+ `wasi-worker-browser.mjs`,
100
+ )
101
+ let needRestrictNodeVersion = true
102
+ if (scopedPackageJson.engines?.node) {
103
+ try {
104
+ const { major } = parse(scopedPackageJson.engines.node) ?? {
105
+ major: 0,
106
+ }
107
+ if (major >= 14) {
108
+ needRestrictNodeVersion = false
109
+ }
110
+ } catch {
111
+ // ignore
112
+ }
113
+ }
114
+ if (needRestrictNodeVersion) {
115
+ scopedPackageJson.engines = {
116
+ node: '>=14.0.0',
117
+ }
118
+ }
119
+ const wasmRuntime = await fetch(
120
+ `https://registry.npmjs.org/@napi-rs/wasm-runtime`,
121
+ ).then((res) => res.json() as Promise<PackageMeta>)
122
+ // @ts-expect-error
123
+ scopedPackageJson.dependencies = {
124
+ '@napi-rs/wasm-runtime': `^${wasmRuntime['dist-tags'].latest}`,
125
+ }
126
+ }
77
127
 
78
- // Only works with yarn 3.1+
79
- // https://github.com/yarnpkg/berry/pull/3981
80
128
  if (target.abi === 'gnu') {
81
129
  // @ts-expect-error
82
130
  scopedPackageJson.libc = ['glibc']
@@ -88,12 +136,12 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
88
136
  const targetPackageJson = join(targetDir, 'package.json')
89
137
  await writeFileAsync(
90
138
  targetPackageJson,
91
- JSON.stringify(scopedPackageJson, null, 2),
139
+ JSON.stringify(scopedPackageJson, null, 2) + '\n',
92
140
  )
93
141
  const targetReadme = join(targetDir, 'README.md')
94
142
  await writeFileAsync(targetReadme, readme(packageName, target))
95
143
 
96
- debug.info(`${packageName}-${target.platformArchABI} created`)
144
+ debug.info(`${packageName} -${target.platformArchABI} created`)
97
145
  }
98
146
  }
99
147
 
package/src/api/new.ts CHANGED
@@ -1,4 +1,10 @@
1
- import path from 'path'
1
+ import { exec, execSync } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { homedir } from 'node:os'
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'
2
8
 
3
9
  import {
4
10
  applyDefaultNewOptions,
@@ -6,35 +12,289 @@ import {
6
12
  } from '../def/new.js'
7
13
  import {
8
14
  AVAILABLE_TARGETS,
9
- CLI_VERSION,
10
15
  debugFactory,
11
16
  DEFAULT_TARGETS,
12
17
  mkdirAsync,
13
- writeFileAsync,
18
+ readdirAsync,
19
+ statAsync,
20
+ SupportedPackageManager,
14
21
  } from '../utils/index.js'
15
22
  import { napiEngineRequirement } from '../utils/version.js'
23
+ import { renameProject } from './rename.js'
16
24
 
17
- import {
18
- createBuildRs,
19
- createCargoToml,
20
- createGithubActionsCIYml,
21
- createLibRs,
22
- createPackageJson,
23
- gitIgnore,
24
- npmIgnore,
25
- } from './templates/index.js'
25
+ // Template imports removed as we're now using external templates
26
26
 
27
27
  const debug = debugFactory('new')
28
28
 
29
- interface Output {
30
- target: string
31
- 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
32
63
  }
33
64
 
34
- 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
+ }
35
292
 
36
293
  function processOptions(options: RawNewOptions) {
37
294
  debug('Processing options...')
295
+ if (!options.path) {
296
+ throw new Error('Please provide the path as the argument')
297
+ }
38
298
  options.path = path.resolve(process.cwd(), options.path)
39
299
  debug(`Resolved target path to: ${options.path}`)
40
300
 
@@ -54,6 +314,20 @@ function processOptions(options: RawNewOptions) {
54
314
  throw new Error('At least one target must be enabled')
55
315
  }
56
316
  }
317
+ if (
318
+ options.targets.some((target) => target === 'wasm32-wasi-preview1-threads')
319
+ ) {
320
+ const out = execSync(`rustup target list`, {
321
+ encoding: 'utf8',
322
+ })
323
+ if (out.includes('wasm32-wasip1-threads')) {
324
+ options.targets = options.targets.map((target) =>
325
+ target === 'wasm32-wasi-preview1-threads'
326
+ ? 'wasm32-wasip1-threads'
327
+ : target,
328
+ )
329
+ }
330
+ }
57
331
 
58
332
  return applyDefaultNewOptions(options) as NewOptions
59
333
  }
@@ -67,131 +341,124 @@ export async function newProject(userOptions: RawNewOptions) {
67
341
  debug('Targets to be enabled:')
68
342
  debug(options.targets)
69
343
 
70
- const outputs = generateFiles(options)
71
-
72
- try {
73
- debug(`Try to create target directory: ${options.path}`)
74
- if (!options.dryRun) {
75
- await mkdirAsync(options.path, { recursive: true })
76
- }
77
- } catch (e) {
78
- throw new Error(`Failed to create target directory: ${options.path}`, {
79
- cause: e,
80
- })
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
+ )
81
349
  }
82
350
 
83
- await dumpOutputs(outputs, options.dryRun)
84
- debug(`Project created at: ${options.path}`)
85
- }
351
+ const packageManager = options.packageManager as SupportedPackageManager
86
352
 
87
- function generateFiles(options: NewOptions): Output[] {
88
- return [
89
- generateCargoToml,
90
- generateLibRs,
91
- generateBuildRs,
92
- generatePackageJson,
93
- generateGithubWorkflow,
94
- generateIgnoreFiles,
95
- ].flatMap((generator) => {
96
- const output = generator(options)
97
-
98
- if (!output) {
99
- return []
100
- }
353
+ // Ensure target directory exists and is empty
354
+ await ensurePath(options.path, options.dryRun)
101
355
 
102
- if (Array.isArray(output)) {
103
- return output.map((o) => ({
104
- ...o,
105
- target: path.join(options.path, o.target),
106
- }))
107
- } else {
108
- return [{ ...output, target: path.join(options.path, output.target) }]
109
- }
110
- })
111
- }
356
+ if (!options.dryRun) {
357
+ try {
358
+ // Download or update template
359
+ const cacheDir = await ensureCacheDir(packageManager)
360
+ await downloadTemplate(packageManager, cacheDir)
112
361
 
113
- function generateCargoToml(options: NewOptions): Output {
114
- return {
115
- target: './Cargo.toml',
116
- content: createCargoToml({
117
- name: options.name,
118
- license: options.license,
119
- features: [`napi${options.minNodeApiVersion}`],
120
- deriveFeatures: options.enableTypeDef ? ['type-def'] : [],
121
- }),
122
- }
123
- }
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
+ )
124
369
 
125
- function generateLibRs(_options: NewOptions): Output {
126
- return {
127
- target: './src/lib.rs',
128
- content: createLibRs(),
129
- }
130
- }
370
+ // Rename project using the rename API
371
+ await renameProject({
372
+ cwd: options.path,
373
+ name: options.name,
374
+ binaryName: getBinaryName(options.name),
375
+ })
131
376
 
132
- function generateBuildRs(_options: NewOptions): Output {
133
- return {
134
- target: './build.rs',
135
- content: createBuildRs(),
136
- }
137
- }
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
+ }
138
382
 
139
- function generatePackageJson(options: NewOptions): Output {
140
- return {
141
- target: './package.json',
142
- content: createPackageJson({
143
- name: options.name,
144
- binaryName: getBinaryName(options.name),
145
- targets: options.targets,
146
- license: options.license,
147
- engineRequirement: napiEngineRequirement(options.minNodeApiVersion),
148
- cliVersion: CLI_VERSION,
149
- }),
150
- }
151
- }
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
+ }
152
397
 
153
- function generateGithubWorkflow(options: NewOptions): Output | null {
154
- if (!options.enableGithubActions) {
155
- return null
156
- }
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)
157
407
 
158
- return {
159
- target: './.github/workflows/ci.yml',
160
- content: createGithubActionsCIYml(options.targets),
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
+ }
161
428
  }
162
- }
163
429
 
164
- function generateIgnoreFiles(_options: NewOptions): Output[] {
165
- return [
166
- {
167
- target: './.gitignore',
168
- content: gitIgnore,
169
- },
170
- {
171
- target: './.npmignore',
172
- content: npmIgnore,
173
- },
174
- ]
430
+ debug(`Project created at: ${options.path}`)
175
431
  }
176
432
 
177
- async function dumpOutputs(outputs: Output[], dryRun?: boolean) {
178
- for (const output of outputs) {
179
- if (!output) {
180
- continue
181
- }
433
+ async function ensurePath(path: string, dryRun = false) {
434
+ const stat = await statAsync(path, {}).catch(() => undefined)
182
435
 
183
- debug(`Writing project file: ${output.target}`)
184
- // only output content to logger instead of writing to file system
185
- if (dryRun) {
186
- debug(output.content)
187
- continue
436
+ // file descriptor exists
437
+ if (stat) {
438
+ if (stat.isFile()) {
439
+ throw new Error(
440
+ `Path ${path} for creating new napi-rs project already exists and it's not a directory.`,
441
+ )
442
+ } else if (stat.isDirectory()) {
443
+ const files = await readdirAsync(path)
444
+ if (files.length) {
445
+ throw new Error(
446
+ `Path ${path} for creating new napi-rs project already exists and it's not empty.`,
447
+ )
448
+ }
188
449
  }
450
+ }
189
451
 
452
+ if (!dryRun) {
190
453
  try {
191
- await mkdirAsync(path.dirname(output.target), { recursive: true })
192
- await writeFileAsync(output.target, output.content, 'utf-8')
454
+ debug(`Try to create target directory: ${path}`)
455
+ if (!dryRun) {
456
+ await mkdirAsync(path, { recursive: true })
457
+ }
193
458
  } catch (e) {
194
- throw new Error(`Failed to write file: ${output.target}`, { cause: e })
459
+ throw new Error(`Failed to create target directory: ${path}`, {
460
+ cause: e,
461
+ })
195
462
  }
196
463
  }
197
464
  }