@znight/cli 0.6.1 → 0.6.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/README.md +61 -27
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +17 -6
- package/dist/index.mjs +1 -1
- package/dist/{main-DUyElIVZ.mjs → main-CLoS12u4.mjs} +452 -75
- package/dist/main-CLoS12u4.mjs.map +1 -0
- package/package.json +7 -7
- package/src/commands/dev.ts +23 -6
- package/src/commands/generate.ts +36 -27
- package/src/commands/new.ts +41 -8
- package/src/commands/start.ts +7 -2
- package/src/dev/supervisor.ts +38 -14
- package/src/scaffold/options.ts +37 -5
- package/src/scaffold/templates.ts +379 -24
- package/src/scaffold/versions.ts +2 -0
- package/dist/main-DUyElIVZ.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@znight/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Znight CLI: scaffold projects, generate code, run dev server and builds",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ugho STEPHAN",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@clack/prompts": "^1.8.1",
|
|
44
|
-
"@znight/core": "0.6.
|
|
44
|
+
"@znight/core": "0.6.3",
|
|
45
45
|
"citty": "^0.2.2",
|
|
46
46
|
"picocolors": "^1.1.1"
|
|
47
47
|
},
|
|
@@ -50,14 +50,14 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"vite": "8.3.5",
|
|
53
|
-
"@znight/compiler": "0.6.
|
|
54
|
-
"@znight/openapi": "0.6.
|
|
55
|
-
"@znight/schedule": "0.6.
|
|
53
|
+
"@znight/compiler": "0.6.3",
|
|
54
|
+
"@znight/openapi": "0.6.3",
|
|
55
|
+
"@znight/schedule": "0.6.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@znight/compiler": "^0.6.
|
|
58
|
+
"@znight/compiler": "^0.6.3",
|
|
59
59
|
"vite": "^8.0.0",
|
|
60
|
-
"@znight/openapi": "^0.6.
|
|
60
|
+
"@znight/openapi": "^0.6.3"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"@znight/compiler": {
|
package/src/commands/dev.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { resolve } from 'node:path'
|
|
|
16
16
|
import { defineCommand } from '../command.ts'
|
|
17
17
|
import { ORPHAN_GUARD_POLL_MS, orphanGuard } from '../dev/orphan.ts'
|
|
18
18
|
import { AppSupervisor } from '../dev/supervisor.ts'
|
|
19
|
-
import { CliError } from '../errors.ts'
|
|
19
|
+
import { CliError, errorMessage } from '../errors.ts'
|
|
20
20
|
import { fallbackFormatDiagnostics } from '../integrations/compiler.ts'
|
|
21
21
|
import { exitCodeOf } from '../integrations/process.ts'
|
|
22
22
|
import { createOutputTracker, formatBuildError, watchBuild } from '../integrations/vite.ts'
|
|
@@ -221,6 +221,16 @@ async function printDiagnostics(cli: CliContext, root: string, diagnostics: read
|
|
|
221
221
|
cli.reporter.logError(text.trimEnd())
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/**
|
|
225
|
+
* The signal stopping the application when the CLI received `signal`. On Windows the application
|
|
226
|
+
* shares the console: it received Ctrl+C and the console closing itself, and `kill()` would be a
|
|
227
|
+
* forced termination, so it is only waited for (`null`).
|
|
228
|
+
*/
|
|
229
|
+
function stopSignal(cli: CliContext, signal: NodeJS.Signals): NodeJS.Signals | null {
|
|
230
|
+
if (cli.runtime.platform === 'win32' && (signal === 'SIGINT' || signal === 'SIGHUP')) return null
|
|
231
|
+
return signal === 'SIGINT' ? 'SIGINT' : 'SIGTERM'
|
|
232
|
+
}
|
|
233
|
+
|
|
224
234
|
function waitForTermination(cli: CliContext, watcher: BuildWatcher, supervisor: AppSupervisor): Promise<number> {
|
|
225
235
|
return new Promise<number>(resolveExit => {
|
|
226
236
|
const dispose = onTermination(
|
|
@@ -229,14 +239,21 @@ function waitForTermination(cli: CliContext, watcher: BuildWatcher, supervisor:
|
|
|
229
239
|
// SIGHUP: the terminal is gone, nothing can be printed
|
|
230
240
|
if (signal !== 'SIGHUP') cli.reporter.info(cli.colors.dim('Stopping... (press Ctrl+C again to force)'))
|
|
231
241
|
void (async () => {
|
|
242
|
+
// the application stops gracefully on SIGINT or SIGTERM; stop() refuses new starts right
|
|
243
|
+
// away, so a restart still debounced cannot start while Vite closes its watcher
|
|
244
|
+
const stopped = supervisor.stop(stopSignal(cli, signal))
|
|
232
245
|
try {
|
|
233
246
|
await watcher.close()
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
247
|
+
} catch (error) {
|
|
248
|
+
cli.reporter.error(`Cannot close the Vite build watcher: ${errorMessage(error)}`)
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
await stopped
|
|
252
|
+
} catch (error) {
|
|
253
|
+
cli.reporter.error(`Cannot stop the application: ${errorMessage(error)}`)
|
|
239
254
|
}
|
|
255
|
+
dispose()
|
|
256
|
+
resolveExit(0)
|
|
240
257
|
})()
|
|
241
258
|
},
|
|
242
259
|
() => {
|
package/src/commands/generate.ts
CHANGED
|
@@ -248,11 +248,14 @@ async function watch(cli: CliContext, compiler: CompilerApi, options: ZnightOpti
|
|
|
248
248
|
const incremental: IncrementalCompiler = compiler.createIncremental(options)
|
|
249
249
|
let state: WatchState = { known: new Set(), roots: [] }
|
|
250
250
|
let pending = new Set<string>()
|
|
251
|
-
let running
|
|
251
|
+
let running: Promise<void> | undefined
|
|
252
|
+
// set on Ctrl+C: a generation in flight must neither watch again nor loop
|
|
253
|
+
let closed = false
|
|
252
254
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
253
255
|
|
|
254
256
|
// which events matter is the compiler's call: the CLI only tells their kind, which fs.watch does not
|
|
255
257
|
const watchers = new TargetWatchers(cli.services.watch, file => {
|
|
258
|
+
if (closed) return
|
|
256
259
|
const path = realPath(file)
|
|
257
260
|
if (!incremental.isRelevantChange(changeKind(path, state.known), path, state.known, state.roots)) return
|
|
258
261
|
pending.add(path)
|
|
@@ -263,33 +266,36 @@ async function watch(cli: CliContext, compiler: CompilerApi, options: ZnightOpti
|
|
|
263
266
|
}, 50)
|
|
264
267
|
})
|
|
265
268
|
|
|
266
|
-
const run =
|
|
267
|
-
if (running) return
|
|
268
|
-
running =
|
|
269
|
+
const run = (initial: boolean): Promise<void> => {
|
|
270
|
+
if (running !== undefined || closed) return running ?? Promise.resolve()
|
|
271
|
+
running = generations(initial).finally(() => {
|
|
272
|
+
running = undefined
|
|
273
|
+
})
|
|
274
|
+
return running
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const generations = async (initial: boolean): Promise<void> => {
|
|
269
278
|
let first = initial
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
known = new Set(result.watchFiles.map(file => toPosixPath(resolve(file))))
|
|
280
|
-
}
|
|
281
|
-
report(cli, compiler, result, root, performance.now() - started)
|
|
282
|
-
} catch (error) {
|
|
283
|
-
cli.reporter.error(`Generation failed: ${errorMessage(error)}`)
|
|
279
|
+
do {
|
|
280
|
+
const files = pending
|
|
281
|
+
pending = new Set()
|
|
282
|
+
const started = performance.now()
|
|
283
|
+
let known = state.known
|
|
284
|
+
try {
|
|
285
|
+
const result = await incremental.generate(first ? undefined : classifyChanges(files, state.known))
|
|
286
|
+
if (result.watchFiles !== undefined) {
|
|
287
|
+
known = new Set(result.watchFiles.map(file => toPosixPath(resolve(file))))
|
|
284
288
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
289
|
+
report(cli, compiler, result, root, performance.now() - started)
|
|
290
|
+
} catch (error) {
|
|
291
|
+
cli.reporter.error(`Generation failed: ${errorMessage(error)}`)
|
|
292
|
+
}
|
|
293
|
+
if (closed) return
|
|
294
|
+
// the include directories of the source libraries are known once compiled
|
|
295
|
+
state = { known, roots: incremental.includeRoots().map(realPath) }
|
|
296
|
+
watchers.update(watchTargets(realRoot, state.roots, state.known), files)
|
|
297
|
+
first = false
|
|
298
|
+
} while (pending.size > 0 && !closed)
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
await run(true)
|
|
@@ -299,10 +305,13 @@ async function watch(cli: CliContext, compiler: CompilerApi, options: ZnightOpti
|
|
|
299
305
|
const dispose = onTermination(
|
|
300
306
|
cli,
|
|
301
307
|
() => {
|
|
308
|
+
closed = true
|
|
302
309
|
if (timer !== undefined) clearTimeout(timer)
|
|
303
310
|
watchers.close()
|
|
304
311
|
dispose()
|
|
305
|
-
|
|
312
|
+
// the generation in flight finishes writing its files first
|
|
313
|
+
const exit = (): void => resolveExit(0)
|
|
314
|
+
void (running ?? Promise.resolve()).then(exit, exit)
|
|
306
315
|
},
|
|
307
316
|
() => undefined,
|
|
308
317
|
)
|
package/src/commands/new.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Command } from '../command.ts'
|
|
3
3
|
import type { CliContext } from '../context.ts'
|
|
4
4
|
import type { Prompts } from '../integrations/prompts.ts'
|
|
5
|
-
import type { Feature, HttpAdapterName, PackageManager } from '../scaffold/options.ts'
|
|
5
|
+
import type { Feature, HttpAdapterName, OpenApiUiName, PackageManager } from '../scaffold/options.ts'
|
|
6
6
|
|
|
7
7
|
import { basename, resolve } from 'node:path'
|
|
8
8
|
|
|
@@ -12,12 +12,16 @@ import { toPackageName, unscopedName, validatePackageName } from '../names.ts'
|
|
|
12
12
|
import { initGit, installDependencies, targetState, writeProject } from '../scaffold/create.ts'
|
|
13
13
|
import {
|
|
14
14
|
ADAPTER_LABELS,
|
|
15
|
+
DEFAULT_ADAPTER,
|
|
15
16
|
DEFAULT_FEATURES,
|
|
17
|
+
DEFAULT_OPENAPI_UI,
|
|
16
18
|
detectPackageManager,
|
|
17
19
|
FEATURE_LABELS,
|
|
18
20
|
FEATURES,
|
|
19
21
|
HTTP_ADAPTERS,
|
|
20
22
|
installCommand,
|
|
23
|
+
OPENAPI_UI_LABELS,
|
|
24
|
+
OPENAPI_UIS,
|
|
21
25
|
PACKAGE_MANAGERS,
|
|
22
26
|
parseFeatures,
|
|
23
27
|
runScript,
|
|
@@ -34,7 +38,8 @@ export const newCommand: Command = defineCommand({
|
|
|
34
38
|
aliases: ['create', 'init'],
|
|
35
39
|
examples: [
|
|
36
40
|
'znight new my-service',
|
|
37
|
-
'znight new my-service --yes --pm pnpm --adapter
|
|
41
|
+
'znight new my-service --yes --pm pnpm --adapter hono --features config,observability,messaging',
|
|
42
|
+
'znight new my-api --yes --features config,observability,openapi,graphql,websocket --openapi-ui swagger',
|
|
38
43
|
'znight new my-worker --yes --features config,schedule',
|
|
39
44
|
'znight new . --yes --no-install',
|
|
40
45
|
],
|
|
@@ -53,15 +58,21 @@ export const newCommand: Command = defineCommand({
|
|
|
53
58
|
},
|
|
54
59
|
adapter: {
|
|
55
60
|
type: 'enum',
|
|
56
|
-
options: ['
|
|
57
|
-
description: 'HTTP adapter (default: node
|
|
61
|
+
options: ['fastify', 'node', 'express', 'hono'],
|
|
62
|
+
description: 'HTTP adapter (default: fastify; node is the built-in node:http server)',
|
|
58
63
|
},
|
|
59
64
|
features: {
|
|
60
65
|
type: 'string',
|
|
61
66
|
valueHint: 'list',
|
|
62
67
|
description:
|
|
63
|
-
'Comma-separated features: config, observability, messaging, schedule, openapi, or "all" / "none" (default: config,observability)',
|
|
68
|
+
'Comma-separated features: config, observability, messaging, schedule, openapi, graphql, websocket, or "all" / "none" (default: config,observability)',
|
|
64
69
|
},
|
|
70
|
+
'openapi-ui': {
|
|
71
|
+
type: 'enum',
|
|
72
|
+
options: ['scalar', 'swagger'],
|
|
73
|
+
description: 'API reference UI of the openapi feature, served at /docs (default: scalar)',
|
|
74
|
+
},
|
|
75
|
+
docker: { type: 'boolean', default: true, description: 'Do not generate a Dockerfile' },
|
|
65
76
|
install: { type: 'boolean', default: true, description: 'Do not install the dependencies' },
|
|
66
77
|
git: { type: 'boolean', default: true, description: 'Do not initialise a git repository' },
|
|
67
78
|
dir: { type: 'string', valueHint: 'path', description: 'Target directory (default: ./<name>)' },
|
|
@@ -112,10 +123,24 @@ export const newCommand: Command = defineCommand({
|
|
|
112
123
|
(await prompts?.select({
|
|
113
124
|
message: 'HTTP adapter?',
|
|
114
125
|
choices: HTTP_ADAPTERS.map(value => ({ value, label: ADAPTER_LABELS[value] })),
|
|
115
|
-
initialValue:
|
|
126
|
+
initialValue: DEFAULT_ADAPTER,
|
|
116
127
|
})) ??
|
|
117
|
-
|
|
128
|
+
DEFAULT_ADAPTER
|
|
118
129
|
const features = await chooseFeatures(options.features, prompts)
|
|
130
|
+
const openapiUi: OpenApiUiName =
|
|
131
|
+
options['openapi-ui'] ??
|
|
132
|
+
(features.includes('openapi')
|
|
133
|
+
? await prompts?.select({
|
|
134
|
+
message: 'OpenAPI reference UI (served at /docs)?',
|
|
135
|
+
choices: OPENAPI_UIS.map(value => ({ value, label: OPENAPI_UI_LABELS[value] })),
|
|
136
|
+
initialValue: DEFAULT_OPENAPI_UI,
|
|
137
|
+
})
|
|
138
|
+
: undefined) ??
|
|
139
|
+
DEFAULT_OPENAPI_UI
|
|
140
|
+
const docker =
|
|
141
|
+
prompts === undefined || hasFlag(rawArgs, 'docker')
|
|
142
|
+
? options.docker !== false
|
|
143
|
+
: await prompts.confirm({ message: 'Add a Dockerfile (production image)?', initialValue: true })
|
|
119
144
|
const install =
|
|
120
145
|
prompts === undefined || hasFlag(rawArgs, 'install')
|
|
121
146
|
? options.install !== false
|
|
@@ -126,7 +151,15 @@ export const newCommand: Command = defineCommand({
|
|
|
126
151
|
: await prompts.confirm({ message: 'Initialise a git repository?', initialValue: true })
|
|
127
152
|
|
|
128
153
|
// files
|
|
129
|
-
const files = renderProject({
|
|
154
|
+
const files = renderProject({
|
|
155
|
+
name,
|
|
156
|
+
packageManager,
|
|
157
|
+
adapter,
|
|
158
|
+
features,
|
|
159
|
+
openapiUi,
|
|
160
|
+
docker,
|
|
161
|
+
znightVersion: cli.version,
|
|
162
|
+
})
|
|
130
163
|
writeProject(directory, files)
|
|
131
164
|
const where = displayPath(directory, cli.cwd)
|
|
132
165
|
const summary = `${cli.colors.bold(name)} (${packageManager}, ${adapter}${features.length > 0 ? `, ${features.join(', ')}` : ''})`
|
package/src/commands/start.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* `znight start`: runs the production build with Node.js (or Bun). On POSIX the CLI process is
|
|
3
3
|
* replaced by the runtime (`execve`): same PID, signals and exit code are native (ideal as a
|
|
4
4
|
* container entry point). Elsewhere the application runs as a child process; signals are forwarded
|
|
5
|
-
* and its exit code is
|
|
5
|
+
* (except, on Windows, Ctrl+C and the console closing, which reach it directly) and its exit code is
|
|
6
|
+
* returned.
|
|
6
7
|
*/
|
|
7
8
|
import type { Command } from '../command.ts'
|
|
8
9
|
|
|
@@ -63,8 +64,12 @@ export const startCommand: Command = defineCommand({
|
|
|
63
64
|
return 0
|
|
64
65
|
}
|
|
65
66
|
const child = processes.spawn(executable, argv, { cwd: root, env, detached: true })
|
|
67
|
+
// on Windows the application shares the console: Ctrl+C and the console closing reach it
|
|
68
|
+
// directly, and child.kill() would be TerminateProcess, cutting its graceful shutdown short
|
|
69
|
+
const consoleEvent = (signal: NodeJS.Signals): boolean =>
|
|
70
|
+
cli.runtime.platform === 'win32' && (signal === 'SIGINT' || signal === 'SIGHUP')
|
|
66
71
|
const forward = (signal: NodeJS.Signals) => (): void => {
|
|
67
|
-
child.kill(signal)
|
|
72
|
+
if (!consoleEvent(signal)) child.kill(signal)
|
|
68
73
|
}
|
|
69
74
|
const listeners = (['SIGINT', 'SIGTERM', 'SIGHUP'] as const).map(signal => [signal, forward(signal)] as const)
|
|
70
75
|
for (const [signal, listener] of listeners) cli.signals.on(signal, listener)
|
package/src/dev/supervisor.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* after each successful build. Stopping sends SIGTERM, waits for the exit and sends SIGKILL after a
|
|
4
4
|
* timeout. Restarts are serialised and coalesced: a burst of builds causes one restart with the
|
|
5
5
|
* latest output. A failed build simply does not schedule a restart (and cancels a pending one when
|
|
6
|
-
* its output is broken
|
|
6
|
+
* its output is broken, even one already stopping the previous process), so nothing starts until a
|
|
7
|
+
* build compiles.
|
|
7
8
|
*/
|
|
8
9
|
import type { ChildHandle, ExitStatus } from '../integrations/process.ts'
|
|
9
10
|
|
|
@@ -41,6 +42,12 @@ export class AppSupervisor {
|
|
|
41
42
|
#debounce: ReturnType<typeof setTimeout> | undefined
|
|
42
43
|
#queue: Promise<void> = Promise.resolve()
|
|
43
44
|
#queued = false
|
|
45
|
+
/** A restart is stopping the previous process before starting the next one. */
|
|
46
|
+
#restarting = false
|
|
47
|
+
/** Bumped by {@link cancelRestart}: a restart requested in an older epoch does not start. */
|
|
48
|
+
#epoch = 0
|
|
49
|
+
/** Epoch of the latest restart request. */
|
|
50
|
+
#requested = 0
|
|
44
51
|
#closed = false
|
|
45
52
|
#started = 0
|
|
46
53
|
|
|
@@ -77,14 +84,18 @@ export class AppSupervisor {
|
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
/**
|
|
80
|
-
* Cancels
|
|
81
|
-
*
|
|
87
|
+
* Cancels the restarts that did not start their process yet (the output they would run turned out
|
|
88
|
+
* to be broken, e.g. a failed compilation): scheduled, queued, or stopping the previous process
|
|
89
|
+
* (which still stops). Returns whether one was pending.
|
|
82
90
|
*/
|
|
83
91
|
cancelRestart(): boolean {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
const pending = this.#debounce !== undefined || this.#queued || this.#restarting
|
|
93
|
+
if (this.#debounce !== undefined) {
|
|
94
|
+
clearTimeout(this.#debounce)
|
|
95
|
+
this.#debounce = undefined
|
|
96
|
+
}
|
|
97
|
+
this.#epoch++
|
|
98
|
+
return pending
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
/**
|
|
@@ -93,14 +104,23 @@ export class AppSupervisor {
|
|
|
93
104
|
*/
|
|
94
105
|
restart(): Promise<void> {
|
|
95
106
|
if (this.#closed) return this.#queue
|
|
107
|
+
this.#requested = this.#epoch
|
|
96
108
|
if (this.#queued) return this.#queue
|
|
97
109
|
this.#queued = true
|
|
98
110
|
this.#queue = this.#queue
|
|
99
111
|
.then(async () => {
|
|
100
112
|
this.#queued = false
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
// cancelled while it waited for the previous restart
|
|
114
|
+
if (this.#closed || this.#requested !== this.#epoch) return
|
|
115
|
+
const epoch = this.#epoch
|
|
116
|
+
this.#restarting = true
|
|
117
|
+
try {
|
|
118
|
+
await this.#stopCurrent('SIGTERM')
|
|
119
|
+
} finally {
|
|
120
|
+
this.#restarting = false
|
|
121
|
+
}
|
|
122
|
+
// cancelled while the previous process was stopping
|
|
123
|
+
if (this.#closed || epoch !== this.#epoch) return
|
|
104
124
|
this.#startNew()
|
|
105
125
|
})
|
|
106
126
|
.catch((error: unknown) => {
|
|
@@ -111,8 +131,12 @@ export class AppSupervisor {
|
|
|
111
131
|
return this.#queue
|
|
112
132
|
}
|
|
113
133
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Stops the current process with `signal` (SIGKILL after the timeout) and refuses new starts.
|
|
136
|
+
* `null`: the process already received the signal (a console Ctrl+C on Windows, where `kill()` is
|
|
137
|
+
* a forced termination): it is only waited for, then SIGKILLed after the timeout.
|
|
138
|
+
*/
|
|
139
|
+
async stop(signal: NodeJS.Signals | null = 'SIGTERM'): Promise<void> {
|
|
116
140
|
this.#closed = true
|
|
117
141
|
if (this.#debounce !== undefined) {
|
|
118
142
|
clearTimeout(this.#debounce)
|
|
@@ -149,13 +173,13 @@ export class AppSupervisor {
|
|
|
149
173
|
this.#events.onStart?.(child, restart)
|
|
150
174
|
}
|
|
151
175
|
|
|
152
|
-
async #stopCurrent(signal: NodeJS.Signals): Promise<void> {
|
|
176
|
+
async #stopCurrent(signal: NodeJS.Signals | null): Promise<void> {
|
|
153
177
|
const child = this.#current
|
|
154
178
|
if (child === undefined) return
|
|
155
179
|
this.#current = undefined
|
|
156
180
|
this.#stopping.add(child)
|
|
157
181
|
try {
|
|
158
|
-
child.kill(signal)
|
|
182
|
+
if (signal !== null) child.kill(signal)
|
|
159
183
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
160
184
|
const timeout = new Promise<'timeout'>(resolve => {
|
|
161
185
|
timer = setTimeout(() => resolve('timeout'), this.#killTimeoutMs)
|
package/src/scaffold/options.ts
CHANGED
|
@@ -7,24 +7,44 @@ export type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
|
|
|
7
7
|
export type HttpAdapterName = 'node' | 'fastify' | 'express' | 'hono'
|
|
8
8
|
|
|
9
9
|
/** Optional features of a new project. */
|
|
10
|
-
export type Feature = 'config' | 'observability' | 'messaging' | 'schedule' | 'openapi'
|
|
10
|
+
export type Feature = 'config' | 'observability' | 'messaging' | 'schedule' | 'openapi' | 'graphql' | 'websocket'
|
|
11
|
+
|
|
12
|
+
/** API reference UIs of `@znight/openapi` (its `ui` option). */
|
|
13
|
+
export type OpenApiUiName = 'scalar' | 'swagger'
|
|
11
14
|
|
|
12
15
|
/** Every package manager, in display order. */
|
|
13
16
|
export const PACKAGE_MANAGERS: readonly PackageManager[] = ['bun', 'npm', 'pnpm', 'yarn']
|
|
14
17
|
|
|
15
|
-
/** Every HTTP adapter, in display order. */
|
|
16
|
-
export const HTTP_ADAPTERS: readonly HttpAdapterName[] = ['
|
|
18
|
+
/** Every HTTP adapter, in display order (the default first). */
|
|
19
|
+
export const HTTP_ADAPTERS: readonly HttpAdapterName[] = ['fastify', 'node', 'express', 'hono']
|
|
20
|
+
|
|
21
|
+
/** HTTP adapter used when none is given. */
|
|
22
|
+
export const DEFAULT_ADAPTER: HttpAdapterName = 'fastify'
|
|
17
23
|
|
|
18
24
|
/** Every feature, in display (and canonical) order. */
|
|
19
|
-
export const FEATURES: readonly Feature[] = [
|
|
25
|
+
export const FEATURES: readonly Feature[] = [
|
|
26
|
+
'config',
|
|
27
|
+
'observability',
|
|
28
|
+
'messaging',
|
|
29
|
+
'schedule',
|
|
30
|
+
'openapi',
|
|
31
|
+
'graphql',
|
|
32
|
+
'websocket',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
/** Every OpenAPI reference UI, in display order (the default first). */
|
|
36
|
+
export const OPENAPI_UIS: readonly OpenApiUiName[] = ['scalar', 'swagger']
|
|
37
|
+
|
|
38
|
+
/** OpenAPI reference UI used when none is given. */
|
|
39
|
+
export const DEFAULT_OPENAPI_UI: OpenApiUiName = 'scalar'
|
|
20
40
|
|
|
21
41
|
/** Features selected when none are given. */
|
|
22
42
|
export const DEFAULT_FEATURES: readonly Feature[] = ['config', 'observability']
|
|
23
43
|
|
|
24
44
|
/** Labels shown by the prompts and the help. */
|
|
25
45
|
export const ADAPTER_LABELS: Readonly<Record<HttpAdapterName, string>> = {
|
|
26
|
-
node: 'node:http (built-in, zero dependency)',
|
|
27
46
|
fastify: 'Fastify',
|
|
47
|
+
node: 'node:http (built-in, zero dependency)',
|
|
28
48
|
express: 'Express',
|
|
29
49
|
hono: 'Hono',
|
|
30
50
|
}
|
|
@@ -36,6 +56,14 @@ export const FEATURE_LABELS: Readonly<Record<Feature, string>> = {
|
|
|
36
56
|
messaging: 'Messaging: message consumers and publishing (@znight/messaging)',
|
|
37
57
|
schedule: 'Schedule: cron, interval and timeout jobs (@znight/schedule)',
|
|
38
58
|
openapi: 'OpenAPI: openapi.json and an API reference at /docs (@znight/openapi)',
|
|
59
|
+
graphql: 'GraphQL: code-first schema derived from the TypeScript types (@znight/graphql)',
|
|
60
|
+
websocket: 'WebSocket: gateways served on the HTTP port (@znight/websocket)',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Labels shown by the prompts. */
|
|
64
|
+
export const OPENAPI_UI_LABELS: Readonly<Record<OpenApiUiName, string>> = {
|
|
65
|
+
scalar: 'Scalar',
|
|
66
|
+
swagger: 'Swagger UI',
|
|
39
67
|
}
|
|
40
68
|
|
|
41
69
|
/** Everything that determines the files of a new project. */
|
|
@@ -46,6 +74,10 @@ export interface ProjectOptions {
|
|
|
46
74
|
readonly adapter: HttpAdapterName
|
|
47
75
|
/** Selected features, in canonical order. */
|
|
48
76
|
readonly features: readonly Feature[]
|
|
77
|
+
/** API reference UI served at `/docs` with the `openapi` feature. Default: `'scalar'`. */
|
|
78
|
+
readonly openapiUi?: OpenApiUiName
|
|
79
|
+
/** Generate a multi-stage `Dockerfile` and a `.dockerignore`. Default: `true`. */
|
|
80
|
+
readonly docker?: boolean
|
|
49
81
|
/** Version range base of the `@znight/*` packages (the CLI version). */
|
|
50
82
|
readonly znightVersion: string
|
|
51
83
|
}
|