reset-framework-cli 1.1.4 → 1.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reset-framework-cli",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "type": "module",
5
5
  "description": "Command-line tooling for Reset Framework.",
6
6
  "license": "MIT",
@@ -11,9 +11,9 @@
11
11
  "node": ">=20.19.0"
12
12
  },
13
13
  "dependencies": {
14
- "@reset-framework/native": "1.1.4",
15
- "@reset-framework/schema": "1.1.4",
16
- "@reset-framework/sdk": "1.1.4"
14
+ "@reset-framework/native": "1.1.5",
15
+ "@reset-framework/schema": "1.1.5",
16
+ "@reset-framework/sdk": "1.1.5"
17
17
  },
18
18
  "bin": {
19
19
  "reset-framework-cli": "src/index.js"
@@ -155,13 +155,13 @@ export async function run(context) {
155
155
 
156
156
  const cleanup = registerChildCleanup(children)
157
157
 
158
- try {
159
- await Promise.race([
160
- ...children.map((child, index) =>
161
- waitForProcessExit(child, index === 0 ? "dev process" : "native host")
162
- )
163
- ])
164
- } finally {
165
- cleanup()
166
- }
167
- }
158
+ try {
159
+ await Promise.race([
160
+ ...children.map((child, index) =>
161
+ waitForProcessExit(child, index === 0 ? "dev process" : "native host")
162
+ )
163
+ ])
164
+ } finally {
165
+ await cleanup()
166
+ }
167
+ }
@@ -176,90 +176,7 @@ function App() {
176
176
  export default App
177
177
  `
178
178
 
179
- const rootFrontendRunner = `import { readFileSync } from 'node:fs'
180
- import path from 'node:path'
181
- import { spawn } from 'node:child_process'
182
-
183
- const script = process.argv[2]
184
- if (!script) {
185
- throw new Error('Missing frontend script name.')
186
- }
187
-
188
- const appRoot = process.cwd()
189
- const config = JSON.parse(readFileSync(path.join(appRoot, 'reset.config.json'), 'utf8'))
190
- const frontendDir =
191
- typeof config?.project?.frontendDir === 'string' && config.project.frontendDir.trim() !== ''
192
- ? path.resolve(appRoot, config.project.frontendDir)
193
- : path.join(appRoot, 'frontend')
194
-
195
- function shouldUseShell(command) {
196
- return process.platform === 'win32' && typeof command === 'string' && /\\.(cmd|bat)$/i.test(command)
197
- }
198
-
199
- function resolvePackageManagerCommand() {
200
- const packageJsonPath = path.join(appRoot, 'package.json')
201
- const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
202
- const packageManager = typeof manifest.packageManager === 'string' ? manifest.packageManager.trim() : ''
203
- const name = packageManager.split('@')[0]
204
-
205
- if (name === 'bun') {
206
- return process.platform === 'win32' ? 'bun.exe' : 'bun'
207
- }
208
-
209
- if (name === 'pnpm') {
210
- return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
211
- }
212
-
213
- if (name === 'yarn') {
214
- return process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
215
- }
216
-
217
- if (name === 'npm') {
218
- return process.platform === 'win32' ? 'npm.cmd' : 'npm'
219
- }
220
-
221
- const candidates = [
222
- ['bun.lock', process.platform === 'win32' ? 'bun.exe' : 'bun'],
223
- ['bun.lockb', process.platform === 'win32' ? 'bun.exe' : 'bun'],
224
- ['pnpm-lock.yaml', process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'],
225
- ['yarn.lock', process.platform === 'win32' ? 'yarn.cmd' : 'yarn'],
226
- ['package-lock.json', process.platform === 'win32' ? 'npm.cmd' : 'npm']
227
- ]
228
-
229
- for (const [fileName, command] of candidates) {
230
- try {
231
- readFileSync(path.join(appRoot, fileName), 'utf8')
232
- return command
233
- } catch {}
234
- }
235
-
236
- return process.platform === 'win32' ? 'npm.cmd' : 'npm'
237
- }
238
-
239
- const command = resolvePackageManagerCommand()
240
- const child = spawn(command, ['run', script, '--', ...process.argv.slice(3)], {
241
- cwd: frontendDir,
242
- stdio: 'inherit',
243
- env: process.env,
244
- shell: shouldUseShell(command)
245
- })
246
-
247
- child.once('error', (error) => {
248
- console.error(error)
249
- process.exit(1)
250
- })
251
-
252
- child.once('exit', (code, signal) => {
253
- if (signal) {
254
- process.kill(process.pid, signal)
255
- return
256
- }
257
-
258
- process.exit(code ?? 1)
259
- })
260
- `
261
-
262
- export const description = "Scaffold a starter app with an interactive project setup"
179
+ export const description = "Scaffold a starter app with an interactive project setup"
263
180
 
264
181
  function listTemplates(templatesDir) {
265
182
  return readdirSync(templatesDir, { withFileTypes: true })
@@ -383,9 +300,31 @@ function resolvePackageManagerOption(context) {
383
300
  return "npm"
384
301
  }
385
302
 
386
- function describeInstallCommand(packageManager) {
387
- return [packageManager, ...getInstallCommandArgs(packageManager)].join(" ")
388
- }
303
+ function describeInstallCommand(packageManager) {
304
+ return [packageManager, ...getInstallCommandArgs(packageManager)].join(" ")
305
+ }
306
+
307
+ function createFrontendScriptCommand(packageManager, frontendDir, scriptName) {
308
+ const frontendPath = JSON.stringify(frontendDir.replace(/\\/g, "/"))
309
+
310
+ if (packageManager === "npm") {
311
+ return `npm --workspace ${frontendPath} run ${scriptName} --`
312
+ }
313
+
314
+ if (packageManager === "pnpm") {
315
+ return `pnpm --dir ${frontendPath} run ${scriptName} --`
316
+ }
317
+
318
+ if (packageManager === "yarn") {
319
+ return `yarn --cwd ${frontendPath} run ${scriptName}`
320
+ }
321
+
322
+ if (packageManager === "bun") {
323
+ return `bun --cwd ${frontendPath} run ${scriptName}`
324
+ }
325
+
326
+ throw new Error(`Unsupported package manager: ${packageManager}`)
327
+ }
389
328
 
390
329
  async function ensureWritableTarget(targetDir, force) {
391
330
  if (!existsSync(targetDir)) {
@@ -491,13 +430,13 @@ function createRootPackageJson(options) {
491
430
  base.packageManager = packageManagerVersion
492
431
  }
493
432
 
494
- if (options.frontendDir !== ".") {
495
- base.workspaces = [options.frontendDir]
496
- base.scripts["dev:web"] = "node ./scripts/run-frontend.mjs dev"
497
- base.scripts["build:web"] = "node ./scripts/run-frontend.mjs build"
498
- base.scripts["lint:web"] = "node ./scripts/run-frontend.mjs lint"
499
- base.scripts["preview:web"] = "node ./scripts/run-frontend.mjs preview"
500
- }
433
+ if (options.frontendDir !== ".") {
434
+ base.workspaces = [options.frontendDir]
435
+ base.scripts["dev:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "dev")
436
+ base.scripts["build:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "build")
437
+ base.scripts["lint:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "lint")
438
+ base.scripts["preview:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "preview")
439
+ }
501
440
 
502
441
  return base
503
442
  }
@@ -538,18 +477,14 @@ async function writeRootPackageFiles(options) {
538
477
  "utf8"
539
478
  )
540
479
 
541
- if (options.frontendDir !== ".") {
542
- await writeFile(
543
- path.join(options.targetDir, "package.json"),
544
- JSON.stringify(createRootPackageJson(options), null, 2) + "\n",
545
- "utf8"
546
- )
547
-
548
- const scriptsDir = path.join(options.targetDir, "scripts")
549
- await mkdir(scriptsDir, { recursive: true })
550
- await writeFile(path.join(scriptsDir, "run-frontend.mjs"), rootFrontendRunner, "utf8")
551
- }
552
- }
480
+ if (options.frontendDir !== ".") {
481
+ await writeFile(
482
+ path.join(options.targetDir, "package.json"),
483
+ JSON.stringify(createRootPackageJson(options), null, 2) + "\n",
484
+ "utf8"
485
+ )
486
+ }
487
+ }
553
488
 
554
489
  async function writeProjectFiles(options) {
555
490
  const templateConfig = JSON.parse(await readFile(options.templateConfigPath, "utf8"))
@@ -846,14 +781,9 @@ export async function run(context) {
846
781
  ["plan", "Write Vite config", path.join(targetFrontendDir, "vite.config.ts")],
847
782
  ["plan", "Write TS config", path.join(targetFrontendDir, "tsconfig.app.json")]
848
783
  ])
849
- if (options.frontendDir !== ".") {
850
- printStatusTable([
851
- ["plan", "Write frontend runner", path.join(options.targetDir, "scripts", "run-frontend.mjs")]
852
- ])
853
- }
854
- if (options.styling === "tailwindcss") {
855
- printStatusTable([
856
- ["plan", "Patch package.json", path.join(targetFrontendDir, "package.json")],
784
+ if (options.styling === "tailwindcss") {
785
+ printStatusTable([
786
+ ["plan", "Patch package.json", path.join(targetFrontendDir, "package.json")],
857
787
  ["plan", "Write styles", path.join(targetFrontendDir, "src", "index.css")],
858
788
  ["plan", "Write starter screen", path.join(targetFrontendDir, "src", "App.tsx")]
859
789
  ])
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process"
1
+ import { spawn, spawnSync } from "node:child_process"
2
2
 
3
3
  import { logger } from "./logger.js"
4
4
 
@@ -129,10 +129,10 @@ export async function captureCommandOutput(command, args = [], options = {}) {
129
129
  })
130
130
  }
131
131
 
132
- export function spawnCommand(command, args = [], options = {}) {
133
- const { cwd, dryRun = false, env } = options
134
-
135
- logger.info(`$ ${formatCommand(command, args)}`)
132
+ export function spawnCommand(command, args = [], options = {}) {
133
+ const { cwd, dryRun = false, env } = options
134
+
135
+ logger.info(`$ ${formatCommand(command, args)}`)
136
136
 
137
137
  if (dryRun) {
138
138
  return null
@@ -141,6 +141,7 @@ export function spawnCommand(command, args = [], options = {}) {
141
141
  return spawn(command, args, {
142
142
  cwd,
143
143
  env: env ? { ...process.env, ...env } : process.env,
144
+ detached: process.platform !== "win32",
144
145
  stdio: "inherit",
145
146
  shell: shouldUseShell(command)
146
147
  })
@@ -166,31 +167,120 @@ export function waitForProcessExit(child, label) {
166
167
  })
167
168
  }
168
169
 
169
- export function registerChildCleanup(children) {
170
- const activeChildren = children.filter(Boolean)
171
-
172
- const cleanup = () => {
173
- for (const child of activeChildren) {
174
- if (child.exitCode === null && !child.killed) {
175
- child.kill("SIGTERM")
176
- }
177
- }
178
- }
179
-
180
- const handleSignal = () => {
181
- cleanup()
182
- process.exit(0)
183
- }
184
-
185
- process.on("SIGINT", handleSignal)
186
- process.on("SIGTERM", handleSignal)
187
-
188
- return () => {
189
- process.off("SIGINT", handleSignal)
190
- process.off("SIGTERM", handleSignal)
191
- cleanup()
192
- }
193
- }
170
+ export function registerChildCleanup(children) {
171
+ const activeChildren = children.filter(Boolean)
172
+ let cleanupPromise = null
173
+
174
+ const waitForChildExit = (child, timeoutMs = 3000) =>
175
+ new Promise((resolve) => {
176
+ if (!child || child.exitCode !== null || child.signalCode !== null) {
177
+ resolve(true)
178
+ return
179
+ }
180
+
181
+ const handleExit = () => {
182
+ clearTimeout(timeout)
183
+ child.off("error", handleError)
184
+ resolve(true)
185
+ }
186
+
187
+ const handleError = () => {
188
+ clearTimeout(timeout)
189
+ child.off("exit", handleExit)
190
+ resolve(true)
191
+ }
192
+
193
+ const timeout = setTimeout(() => {
194
+ child.off("exit", handleExit)
195
+ child.off("error", handleError)
196
+ resolve(false)
197
+ }, timeoutMs)
198
+
199
+ child.once("exit", handleExit)
200
+ child.once("error", handleError)
201
+ })
202
+
203
+ const terminateChild = async (child) => {
204
+ if (!child || child.pid == null || child.exitCode !== null || child.signalCode !== null) {
205
+ return
206
+ }
207
+
208
+ if (process.platform === "win32") {
209
+ const result = spawnSync("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], {
210
+ stdio: "ignore",
211
+ windowsHide: true
212
+ })
213
+
214
+ if (result.status !== 0 && child.exitCode === null && child.signalCode === null) {
215
+ try {
216
+ child.kill("SIGTERM")
217
+ } catch {
218
+ // Ignore late shutdown races.
219
+ }
220
+ }
221
+
222
+ await waitForChildExit(child, 2000)
223
+ return
224
+ }
225
+
226
+ try {
227
+ process.kill(-child.pid, "SIGTERM")
228
+ } catch (error) {
229
+ if (error?.code !== "ESRCH") {
230
+ try {
231
+ child.kill("SIGTERM")
232
+ } catch {
233
+ // Ignore late shutdown races.
234
+ }
235
+ }
236
+ }
237
+
238
+ if (await waitForChildExit(child, 3000)) {
239
+ return
240
+ }
241
+
242
+ try {
243
+ process.kill(-child.pid, "SIGKILL")
244
+ } catch (error) {
245
+ if (error?.code !== "ESRCH") {
246
+ try {
247
+ child.kill("SIGKILL")
248
+ } catch {
249
+ // Ignore late shutdown races.
250
+ }
251
+ }
252
+ }
253
+
254
+ await waitForChildExit(child, 1000)
255
+ }
256
+
257
+ const cleanup = async () => {
258
+ if (cleanupPromise) {
259
+ return cleanupPromise
260
+ }
261
+
262
+ cleanupPromise = (async () => {
263
+ await Promise.all(activeChildren.map((child) => terminateChild(child)))
264
+ })()
265
+
266
+ return cleanupPromise
267
+ }
268
+
269
+ const handleSignal = () => {
270
+ void cleanup().finally(() => {
271
+ process.exit(0)
272
+ })
273
+ }
274
+
275
+ process.on("SIGINT", handleSignal)
276
+ process.on("SIGTERM", handleSignal)
277
+
278
+ return async () => {
279
+ process.off("SIGINT", handleSignal)
280
+ process.off("SIGTERM", handleSignal)
281
+ await cleanup()
282
+ }
283
+ }
194
284
 
195
285
  export async function waitForUrl(url, options = {}) {
196
286
  const { intervalMs = 250, timeoutMs = 15000 } = options