reset-framework-cli 1.1.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/output.js CHANGED
@@ -1,13 +1,13 @@
1
- import path from "node:path"
2
- import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises"
3
- import { existsSync } from "node:fs"
4
-
5
- import { logger } from "./logger.js"
6
- import { runCommand } from "./process.js"
7
- import {
8
- resolveFrameworkRuntimeBinary,
9
- resolveFrameworkRuntimeOutputDir
10
- } from "./project.js"
1
+ import path from "node:path"
2
+ import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises"
3
+ import { existsSync } from "node:fs"
4
+
5
+ import { logger } from "./logger.js"
6
+ import { runCommand } from "./process.js"
7
+ import {
8
+ resolveFrameworkRuntimeBinary,
9
+ resolveFrameworkRuntimeOutputDir
10
+ } from "./project.js"
11
11
 
12
12
  function escapePlistString(value) {
13
13
  return value
@@ -60,7 +60,7 @@ function serializePlistValue(value, indentLevel = 1) {
60
60
  return `${indent}<string>${escapePlistString(String(value))}</string>\n`
61
61
  }
62
62
 
63
- function buildMacOSInfoPlist(config) {
63
+ function buildMacOSInfoPlist(config) {
64
64
  const urlTypes = Array.isArray(config.protocols?.schemes)
65
65
  ? config.protocols.schemes.map((schemeConfig) => ({
66
66
  CFBundleTypeRole: schemeConfig.role ?? "Viewer",
@@ -95,39 +95,84 @@ function buildMacOSInfoPlist(config) {
95
95
  `<plist version="1.0">\n` +
96
96
  serializePlistValue(plist, 0) +
97
97
  `</plist>\n`
98
- )
99
- }
98
+ )
99
+ }
100
+
101
+ function shouldCopyWindowsRuntimeArtifact(entryName) {
102
+ const extension = path.extname(entryName).toLowerCase()
103
+
104
+ return [
105
+ ".bin",
106
+ ".dat",
107
+ ".dll",
108
+ ".exe",
109
+ ".json",
110
+ ".manifest",
111
+ ".pak",
112
+ ".pri"
113
+ ].includes(extension)
114
+ }
115
+
116
+ function escapePowerShellLiteral(value) {
117
+ return String(value).replaceAll("'", "''")
118
+ }
119
+
120
+ export async function stageMacOSAppBundle(frameworkPaths, frameworkBuildPaths, config, options = {}) {
121
+ const { dryRun = false, outputPaths, configPath, preferSource = false } = options
122
+ const runtimeSourceBundle = resolveFrameworkRuntimeOutputDir(frameworkPaths, frameworkBuildPaths, "release", {
123
+ mustExist: !dryRun,
124
+ preferSource
125
+ })
100
126
 
101
- function shouldCopyWindowsRuntimeArtifact(entryName) {
102
- const extension = path.extname(entryName).toLowerCase()
127
+ if (!outputPaths) {
128
+ throw new Error("stageMacOSAppBundle requires resolved output paths")
129
+ }
103
130
 
104
- return [
105
- ".bin",
106
- ".dat",
107
- ".dll",
108
- ".exe",
109
- ".json",
110
- ".manifest",
111
- ".pak",
112
- ".pri"
113
- ].includes(extension)
114
- }
131
+ if (!existsSync(outputPaths.frontendEntryFile) && !dryRun) {
132
+ throw new Error(
133
+ `Missing frontend build output at ${outputPaths.frontendEntryFile}. Run the frontend build first.`
134
+ )
135
+ }
136
+
137
+ logger.info(`Staging app bundle into ${outputPaths.appBundlePath}`)
115
138
 
116
- function escapePowerShellLiteral(value) {
117
- return String(value).replaceAll("'", "''")
118
- }
139
+ if (dryRun) {
140
+ logger.info(`- copy ${runtimeSourceBundle} -> ${outputPaths.appBundlePath}`)
141
+ logger.info(`- copy ${configPath} -> ${outputPaths.bundledConfigPath}`)
142
+ logger.info(`- copy ${outputPaths.frontendDistDir} -> ${outputPaths.bundledFrontendDir}`)
143
+ return outputPaths
144
+ }
119
145
 
120
- export async function stageMacOSAppBundle(frameworkBuildPaths, appPaths, config, options = {}) {
121
- const { dryRun = false, outputPaths, configPath } = options
146
+ await mkdir(outputPaths.macosDir, { recursive: true })
147
+ await rm(outputPaths.appBundlePath, { recursive: true, force: true })
148
+ await cp(runtimeSourceBundle, outputPaths.appBundlePath, { recursive: true })
122
149
 
123
- if (!outputPaths) {
124
- throw new Error("stageMacOSAppBundle requires resolved output paths")
125
- }
150
+ await mkdir(outputPaths.resourcesDir, { recursive: true })
151
+ await cp(configPath, outputPaths.bundledConfigPath, { force: true })
152
+ await rm(outputPaths.bundledFrontendDir, { recursive: true, force: true })
153
+ await cp(outputPaths.frontendDistDir, outputPaths.bundledFrontendDir, { recursive: true })
154
+ await writeFile(
155
+ path.join(outputPaths.appBundlePath, "Contents", "Info.plist"),
156
+ buildMacOSInfoPlist(config),
157
+ "utf8"
158
+ )
126
159
 
127
- if (!existsSync(frameworkBuildPaths.releaseAppTemplate) && !dryRun) {
128
- throw new Error(
129
- `Missing runtime app bundle at ${frameworkBuildPaths.releaseAppTemplate}. Run reset-framework-cli build first.`
130
- )
160
+ return outputPaths
161
+ }
162
+
163
+ export async function stageWindowsAppBundle(frameworkPaths, frameworkBuildPaths, config, options = {}) {
164
+ const { dryRun = false, outputPaths, configPath, preferSource = false } = options
165
+ const runtimeSourceBinary = resolveFrameworkRuntimeBinary(frameworkPaths, frameworkBuildPaths, "release", {
166
+ mustExist: !dryRun,
167
+ preferSource
168
+ })
169
+ const runtimeSourceDir = resolveFrameworkRuntimeOutputDir(frameworkPaths, frameworkBuildPaths, "release", {
170
+ mustExist: !dryRun,
171
+ preferSource
172
+ })
173
+
174
+ if (!outputPaths) {
175
+ throw new Error("stageWindowsAppBundle requires resolved output paths")
131
176
  }
132
177
 
133
178
  if (!existsSync(outputPaths.frontendEntryFile) && !dryRun) {
@@ -136,91 +181,46 @@ export async function stageMacOSAppBundle(frameworkBuildPaths, appPaths, config,
136
181
  )
137
182
  }
138
183
 
139
- logger.info(`Staging app bundle into ${outputPaths.appBundlePath}`)
184
+ logger.info(`Staging Windows app into ${outputPaths.appBundlePath}`)
140
185
 
141
186
  if (dryRun) {
142
- logger.info(`- copy ${frameworkBuildPaths.releaseAppTemplate} -> ${outputPaths.appBundlePath}`)
187
+ logger.info(`- copy ${runtimeSourceDir} -> ${outputPaths.appBundlePath}`)
188
+ logger.info(`- rename ${runtimeSourceBinary} -> ${outputPaths.appExecutablePath}`)
143
189
  logger.info(`- copy ${configPath} -> ${outputPaths.bundledConfigPath}`)
144
190
  logger.info(`- copy ${outputPaths.frontendDistDir} -> ${outputPaths.bundledFrontendDir}`)
145
191
  return outputPaths
146
192
  }
147
193
 
148
- await mkdir(outputPaths.macosDir, { recursive: true })
194
+ await mkdir(outputPaths.windowsDir, { recursive: true })
149
195
  await rm(outputPaths.appBundlePath, { recursive: true, force: true })
150
- await cp(frameworkBuildPaths.releaseAppTemplate, outputPaths.appBundlePath, { recursive: true })
196
+ await mkdir(outputPaths.appBundlePath, { recursive: true })
197
+ const runtimeEntries = await readdir(runtimeSourceDir, { withFileTypes: true })
198
+ const runtimeBinaryName = path.basename(runtimeSourceBinary)
199
+
200
+ for (const entry of runtimeEntries) {
201
+ if (!entry.isDirectory() && !shouldCopyWindowsRuntimeArtifact(entry.name)) {
202
+ continue
203
+ }
204
+
205
+ const sourcePath = path.join(runtimeSourceDir, entry.name)
206
+ const targetPath =
207
+ entry.name.toLowerCase() === runtimeBinaryName.toLowerCase()
208
+ ? outputPaths.appExecutablePath
209
+ : path.join(outputPaths.appBundlePath, entry.name)
210
+
211
+ await cp(sourcePath, targetPath, { recursive: true, force: true })
212
+ }
151
213
 
152
214
  await mkdir(outputPaths.resourcesDir, { recursive: true })
153
215
  await cp(configPath, outputPaths.bundledConfigPath, { force: true })
154
216
  await rm(outputPaths.bundledFrontendDir, { recursive: true, force: true })
155
217
  await cp(outputPaths.frontendDistDir, outputPaths.bundledFrontendDir, { recursive: true })
156
- await writeFile(
157
- path.join(outputPaths.appBundlePath, "Contents", "Info.plist"),
158
- buildMacOSInfoPlist(config),
159
- "utf8"
160
- )
161
218
 
162
- return outputPaths
163
- }
164
-
165
- export async function stageWindowsAppBundle(frameworkBuildPaths, appPaths, config, options = {}) {
166
- const { dryRun = false, outputPaths, configPath } = options
167
- const runtimeSourceBinary = resolveFrameworkRuntimeBinary(frameworkBuildPaths, "release", {
168
- mustExist: !dryRun
169
- })
170
- const runtimeSourceDir = resolveFrameworkRuntimeOutputDir(frameworkBuildPaths, "release", {
171
- mustExist: !dryRun
172
- })
173
-
174
- if (!outputPaths) {
175
- throw new Error("stageWindowsAppBundle requires resolved output paths")
176
- }
177
-
178
- if (!existsSync(outputPaths.frontendEntryFile) && !dryRun) {
179
- throw new Error(
180
- `Missing frontend build output at ${outputPaths.frontendEntryFile}. Run the frontend build first.`
181
- )
182
- }
183
-
184
- logger.info(`Staging Windows app into ${outputPaths.appBundlePath}`)
185
-
186
- if (dryRun) {
187
- logger.info(`- copy ${runtimeSourceDir} -> ${outputPaths.appBundlePath}`)
188
- logger.info(`- rename ${runtimeSourceBinary} -> ${outputPaths.appExecutablePath}`)
189
- logger.info(`- copy ${configPath} -> ${outputPaths.bundledConfigPath}`)
190
- logger.info(`- copy ${outputPaths.frontendDistDir} -> ${outputPaths.bundledFrontendDir}`)
191
- return outputPaths
192
- }
193
-
194
- await mkdir(outputPaths.windowsDir, { recursive: true })
195
- await rm(outputPaths.appBundlePath, { recursive: true, force: true })
196
- await mkdir(outputPaths.appBundlePath, { recursive: true })
197
- const runtimeEntries = await readdir(runtimeSourceDir, { withFileTypes: true })
198
- const runtimeBinaryName = path.basename(runtimeSourceBinary)
199
-
200
- for (const entry of runtimeEntries) {
201
- if (!entry.isDirectory() && !shouldCopyWindowsRuntimeArtifact(entry.name)) {
202
- continue
203
- }
204
-
205
- const sourcePath = path.join(runtimeSourceDir, entry.name)
206
- const targetPath =
207
- entry.name.toLowerCase() === runtimeBinaryName.toLowerCase()
208
- ? outputPaths.appExecutablePath
209
- : path.join(outputPaths.appBundlePath, entry.name)
210
-
211
- await cp(sourcePath, targetPath, { recursive: true, force: true })
212
- }
213
-
214
- await mkdir(outputPaths.resourcesDir, { recursive: true })
215
- await cp(configPath, outputPaths.bundledConfigPath, { force: true })
216
- await rm(outputPaths.bundledFrontendDir, { recursive: true, force: true })
217
- await cp(outputPaths.frontendDistDir, outputPaths.bundledFrontendDir, { recursive: true })
218
-
219
- return outputPaths
220
- }
219
+ return outputPaths
220
+ }
221
221
 
222
- export async function createMacOSZipArchive(outputPaths, options = {}) {
223
- const { dryRun = false } = options
222
+ export async function createMacOSZipArchive(outputPaths, options = {}) {
223
+ const { dryRun = false } = options
224
224
 
225
225
  logger.info(`Packaging ${outputPaths.appBundlePath} -> ${outputPaths.zipPath}`)
226
226
 
@@ -232,37 +232,37 @@ export async function createMacOSZipArchive(outputPaths, options = {}) {
232
232
  await mkdir(outputPaths.packagesDir, { recursive: true })
233
233
  }
234
234
 
235
- await runCommand(
236
- "ditto",
237
- ["-c", "-k", "--sequesterRsrc", "--keepParent", outputPaths.appBundlePath, outputPaths.zipPath],
238
- { dryRun }
239
- )
240
- }
241
-
242
- export async function createWindowsZipArchive(outputPaths, options = {}) {
243
- const { dryRun = false } = options
244
-
245
- logger.info(`Packaging ${outputPaths.appBundlePath} -> ${outputPaths.zipPath}`)
246
-
247
- if (process.platform !== "win32") {
248
- throw new Error("Packaging is only implemented for Windows and macOS right now")
249
- }
250
-
251
- if (!dryRun) {
252
- await mkdir(outputPaths.packagesDir, { recursive: true })
253
- await rm(outputPaths.zipPath, { force: true })
254
- }
255
-
256
- const literalSourcePath = escapePowerShellLiteral(outputPaths.appBundlePath)
257
- const literalDestinationPath = escapePowerShellLiteral(outputPaths.zipPath)
258
- const archiveCommand = [
259
- "$ErrorActionPreference = 'Stop'",
260
- `Compress-Archive -LiteralPath '${literalSourcePath}' -DestinationPath '${literalDestinationPath}' -CompressionLevel Optimal`
261
- ].join("; ")
262
-
263
- await runCommand(
264
- "powershell.exe",
265
- ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", archiveCommand],
266
- { dryRun }
267
- )
268
- }
235
+ await runCommand(
236
+ "ditto",
237
+ ["-c", "-k", "--sequesterRsrc", "--keepParent", outputPaths.appBundlePath, outputPaths.zipPath],
238
+ { dryRun }
239
+ )
240
+ }
241
+
242
+ export async function createWindowsZipArchive(outputPaths, options = {}) {
243
+ const { dryRun = false } = options
244
+
245
+ logger.info(`Packaging ${outputPaths.appBundlePath} -> ${outputPaths.zipPath}`)
246
+
247
+ if (process.platform !== "win32") {
248
+ throw new Error("Packaging is only implemented for Windows and macOS right now")
249
+ }
250
+
251
+ if (!dryRun) {
252
+ await mkdir(outputPaths.packagesDir, { recursive: true })
253
+ await rm(outputPaths.zipPath, { force: true })
254
+ }
255
+
256
+ const literalSourcePath = escapePowerShellLiteral(outputPaths.appBundlePath)
257
+ const literalDestinationPath = escapePowerShellLiteral(outputPaths.zipPath)
258
+ const archiveCommand = [
259
+ "$ErrorActionPreference = 'Stop'",
260
+ `Compress-Archive -LiteralPath '${literalSourcePath}' -DestinationPath '${literalDestinationPath}' -CompressionLevel Optimal`
261
+ ].join("; ")
262
+
263
+ await runCommand(
264
+ "powershell.exe",
265
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", archiveCommand],
266
+ { dryRun }
267
+ )
268
+ }
@@ -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
 
@@ -44,32 +44,32 @@ export function getInstallCommandArgs(packageManager = "npm") {
44
44
  return ["install"]
45
45
  }
46
46
 
47
- export function formatCommand(command, args = []) {
48
- return [command, ...args].join(" ")
49
- }
50
-
51
- function shouldUseShell(command) {
52
- return process.platform === "win32" && typeof command === "string" && /\.(cmd|bat)$/i.test(command)
53
- }
54
-
55
- export async function runCommand(command, args = [], options = {}) {
56
- const { cwd, dryRun = false, env } = options
57
-
58
- logger.info(`$ ${formatCommand(command, args)}`)
47
+ export function formatCommand(command, args = []) {
48
+ return [command, ...args].join(" ")
49
+ }
50
+
51
+ function shouldUseShell(command) {
52
+ return process.platform === "win32" && typeof command === "string" && /\.(cmd|bat)$/i.test(command)
53
+ }
54
+
55
+ export async function runCommand(command, args = [], options = {}) {
56
+ const { cwd, dryRun = false, env } = options
57
+
58
+ logger.info(`$ ${formatCommand(command, args)}`)
59
59
 
60
60
  if (dryRun) {
61
61
  return
62
62
  }
63
63
 
64
- await new Promise((resolve, reject) => {
65
- const child = spawn(command, args, {
66
- cwd,
67
- env: env ? { ...process.env, ...env } : process.env,
68
- stdio: "inherit",
69
- shell: shouldUseShell(command)
70
- })
71
-
72
- child.once("error", reject)
64
+ await new Promise((resolve, reject) => {
65
+ const child = spawn(command, args, {
66
+ cwd,
67
+ env: env ? { ...process.env, ...env } : process.env,
68
+ stdio: "inherit",
69
+ shell: shouldUseShell(command)
70
+ })
71
+
72
+ child.once("error", reject)
73
73
  child.once("exit", (code, signal) => {
74
74
  if (code === 0) {
75
75
  resolve(undefined)
@@ -92,13 +92,13 @@ export async function captureCommandOutput(command, args = [], options = {}) {
92
92
 
93
93
  await Promise.resolve()
94
94
 
95
- return await new Promise((resolve, reject) => {
96
- const child = spawn(command, args, {
97
- cwd,
98
- env: env ? { ...process.env, ...env } : process.env,
99
- stdio: ["ignore", "pipe", "pipe"],
100
- shell: shouldUseShell(command)
101
- })
95
+ return await new Promise((resolve, reject) => {
96
+ const child = spawn(command, args, {
97
+ cwd,
98
+ env: env ? { ...process.env, ...env } : process.env,
99
+ stdio: ["ignore", "pipe", "pipe"],
100
+ shell: shouldUseShell(command)
101
+ })
102
102
 
103
103
  let stdout = ""
104
104
  let stderr = ""
@@ -138,13 +138,14 @@ export function spawnCommand(command, args = [], options = {}) {
138
138
  return null
139
139
  }
140
140
 
141
- return spawn(command, args, {
142
- cwd,
143
- env: env ? { ...process.env, ...env } : process.env,
144
- stdio: "inherit",
145
- shell: shouldUseShell(command)
146
- })
147
- }
141
+ return spawn(command, args, {
142
+ cwd,
143
+ env: env ? { ...process.env, ...env } : process.env,
144
+ detached: process.platform !== "win32",
145
+ stdio: "inherit",
146
+ shell: shouldUseShell(command)
147
+ })
148
+ }
148
149
 
149
150
  export function waitForProcessExit(child, label) {
150
151
  return new Promise((resolve, reject) => {
@@ -168,27 +169,116 @@ export function waitForProcessExit(child, label) {
168
169
 
169
170
  export function registerChildCleanup(children) {
170
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
+ }
171
180
 
172
- const cleanup = () => {
173
- for (const child of activeChildren) {
174
- if (child.exitCode === null && !child.killed) {
175
- child.kill("SIGTERM")
181
+ const handleExit = () => {
182
+ clearTimeout(timeout)
183
+ child.off("error", handleError)
184
+ resolve(true)
176
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
177
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
178
267
  }
179
268
 
180
269
  const handleSignal = () => {
181
- cleanup()
182
- process.exit(0)
270
+ void cleanup().finally(() => {
271
+ process.exit(0)
272
+ })
183
273
  }
184
274
 
185
275
  process.on("SIGINT", handleSignal)
186
276
  process.on("SIGTERM", handleSignal)
187
277
 
188
- return () => {
278
+ return async () => {
189
279
  process.off("SIGINT", handleSignal)
190
280
  process.off("SIGTERM", handleSignal)
191
- cleanup()
281
+ await cleanup()
192
282
  }
193
283
  }
194
284