@znight/cli 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +482 -2
- package/bin/znight.mjs +17 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +13 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/dev/orphan-guard.d.mts +1 -0
- package/dist/dev/orphan-guard.mjs +47 -0
- package/dist/dev/orphan-guard.mjs.map +1 -0
- package/dist/index.d.mts +679 -0
- package/dist/index.mjs +1 -0
- package/dist/main-BWsiUsxN.mjs +3865 -0
- package/dist/main-BWsiUsxN.mjs.map +1 -0
- package/dist/orphan-Dn-2xMBd.mjs +45 -0
- package/dist/orphan-Dn-2xMBd.mjs.map +1 -0
- package/package.json +71 -4
- package/src/cli.ts +11 -0
- package/src/command.ts +217 -0
- package/src/commands/build.ts +159 -0
- package/src/commands/dev.ts +249 -0
- package/src/commands/generate.ts +304 -0
- package/src/commands/new.ts +245 -0
- package/src/commands/openapi.ts +160 -0
- package/src/commands/schematic.ts +110 -0
- package/src/commands/shared.ts +106 -0
- package/src/commands/start.ts +79 -0
- package/src/context.ts +126 -0
- package/src/dev/orphan-guard.ts +49 -0
- package/src/dev/orphan.ts +74 -0
- package/src/dev/supervisor.ts +175 -0
- package/src/errors.ts +64 -0
- package/src/index.ts +73 -0
- package/src/integrations/compiler.ts +360 -0
- package/src/integrations/openapi.ts +97 -0
- package/src/integrations/process.ts +180 -0
- package/src/integrations/prompts.ts +108 -0
- package/src/integrations/resolve.ts +38 -0
- package/src/integrations/vite.ts +362 -0
- package/src/integrations/watch.ts +243 -0
- package/src/main.ts +195 -0
- package/src/names.ts +106 -0
- package/src/output.ts +110 -0
- package/src/runtime.ts +86 -0
- package/src/scaffold/create.ts +65 -0
- package/src/scaffold/options.ts +103 -0
- package/src/scaffold/templates.ts +771 -0
- package/src/scaffold/versions.ts +25 -0
- package/src/schematics/index.ts +422 -0
- package/src/version.ts +20 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `znight dev`: Vite build in watch mode (no Vite dev server) and a full restart of the application
|
|
3
|
+
* process after every successful rebuild — no partial hot replacement of providers: a restart is
|
|
4
|
+
* fast and always consistent. A failed build keeps the last good process running, and so does a
|
|
5
|
+
* failed compilation: in watch mode the `znight()` plugin does not fail the build (the bundle
|
|
6
|
+
* contains a module throwing the diagnostics), so its result (`plugin.api.result`) is checked
|
|
7
|
+
* before restarting.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from '../command.ts'
|
|
10
|
+
import type { CliContext } from '../context.ts'
|
|
11
|
+
import type { Diagnostic } from '../integrations/compiler.ts'
|
|
12
|
+
import type { BuildWatcher, ViteConfigLoader, ViteLogLevel, WatcherEvent } from '../integrations/vite.ts'
|
|
13
|
+
|
|
14
|
+
import { resolve } from 'node:path'
|
|
15
|
+
|
|
16
|
+
import { defineCommand } from '../command.ts'
|
|
17
|
+
import { ORPHAN_GUARD_POLL_MS, orphanGuard } from '../dev/orphan.ts'
|
|
18
|
+
import { AppSupervisor } from '../dev/supervisor.ts'
|
|
19
|
+
import { CliError } from '../errors.ts'
|
|
20
|
+
import { fallbackFormatDiagnostics } from '../integrations/compiler.ts'
|
|
21
|
+
import { exitCodeOf } from '../integrations/process.ts'
|
|
22
|
+
import { createOutputTracker, formatBuildError, watchBuild } from '../integrations/vite.ts'
|
|
23
|
+
import { formatDuration, plural } from '../output.ts'
|
|
24
|
+
import { type RuntimeOption, resolveRuntime, runtimeArguments, runtimeExecutable } from '../runtime.ts'
|
|
25
|
+
import { displayPath, onTermination, parseNonNegativeInteger, resolveRoot } from './shared.ts'
|
|
26
|
+
|
|
27
|
+
/** Clears the terminal and its scrollback. */
|
|
28
|
+
const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H'
|
|
29
|
+
|
|
30
|
+
/** `znight dev` */
|
|
31
|
+
export const devCommand: Command = defineCommand({
|
|
32
|
+
meta: {
|
|
33
|
+
name: 'dev',
|
|
34
|
+
description: 'Build in watch mode and restart the application after every successful rebuild',
|
|
35
|
+
usage: '[options] [-- <application arguments>]',
|
|
36
|
+
examples: ['znight dev', 'znight dev --runtime bun --clear', 'znight dev --inspect -- --verbose'],
|
|
37
|
+
details: [
|
|
38
|
+
'Uses "vite build --watch" with the project Vite config (the znight() plugin regenerates the container).',
|
|
39
|
+
'On every successful build the application receives SIGTERM, gets --kill-timeout ms to stop gracefully',
|
|
40
|
+
'(then SIGKILL), and a new process starts. A failed build or compilation (ZNIGHT errors) keeps the',
|
|
41
|
+
'last good process running.',
|
|
42
|
+
].join('\n'),
|
|
43
|
+
},
|
|
44
|
+
args: {
|
|
45
|
+
root: { type: 'string', valueHint: 'dir', description: 'Project root (default: the current directory)' },
|
|
46
|
+
config: { type: 'string', valueHint: 'file', description: 'Vite config file (default: vite.config.*)' },
|
|
47
|
+
mode: { type: 'string', default: 'development', description: 'Vite mode' },
|
|
48
|
+
'config-loader': {
|
|
49
|
+
type: 'enum',
|
|
50
|
+
options: ['bundle', 'runner', 'native'],
|
|
51
|
+
description: 'How Vite loads the config file (default: bundle)',
|
|
52
|
+
},
|
|
53
|
+
runtime: {
|
|
54
|
+
type: 'enum',
|
|
55
|
+
options: ['auto', 'node', 'bun'],
|
|
56
|
+
default: 'auto',
|
|
57
|
+
description: 'Runtime of the application (auto: the runtime running the CLI)',
|
|
58
|
+
},
|
|
59
|
+
entry: { type: 'string', valueHint: 'file', description: 'File to run (default: the entry chunk of the build)' },
|
|
60
|
+
inspect: { type: 'boolean', description: 'Start the application with --inspect (debugger)' },
|
|
61
|
+
clear: { type: 'boolean', description: 'Clear the screen before each rebuild' },
|
|
62
|
+
debounce: { type: 'string', valueHint: 'ms', default: '100', description: 'Delay before restarting after a build' },
|
|
63
|
+
'kill-timeout': {
|
|
64
|
+
type: 'string',
|
|
65
|
+
valueHint: 'ms',
|
|
66
|
+
default: '5000',
|
|
67
|
+
description: 'Time the application gets to stop after SIGTERM before SIGKILL',
|
|
68
|
+
},
|
|
69
|
+
'log-level': {
|
|
70
|
+
type: 'enum',
|
|
71
|
+
options: ['info', 'warn', 'error', 'silent'],
|
|
72
|
+
default: 'warn',
|
|
73
|
+
description: 'Vite log level',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
async run({ args, cli, passthrough }) {
|
|
77
|
+
const root = resolveRoot(cli, args.root)
|
|
78
|
+
const debounceMs = parseNonNegativeInteger(args.debounce, 'debounce', 100)
|
|
79
|
+
const killTimeoutMs = parseNonNegativeInteger(args['kill-timeout'], 'kill-timeout', 5000)
|
|
80
|
+
const runtime = resolveRuntime(args.runtime as RuntimeOption, cli.runtime)
|
|
81
|
+
const executable = runtimeExecutable(runtime, cli.runtime, cli.env)
|
|
82
|
+
if (executable === undefined) {
|
|
83
|
+
throw new CliError(`Cannot find "${runtime}" in PATH`, { hints: ['install it, or choose another --runtime'] })
|
|
84
|
+
}
|
|
85
|
+
const logLevel = args['log-level'] as ViteLogLevel
|
|
86
|
+
const { colors, reporter } = cli
|
|
87
|
+
// before Vite runs: its build sets process.env.NODE_ENV (to "production") when it is not set
|
|
88
|
+
const env = { ...cli.env }
|
|
89
|
+
// the application runs detached: it stops by itself when the CLI dies without stopping it (kill -9)
|
|
90
|
+
const guard = orphanGuard(runtime, { parentPid: process.pid, pollMs: ORPHAN_GUARD_POLL_MS, killTimeoutMs })
|
|
91
|
+
const runtimeArgs = runtimeArguments(runtime, [
|
|
92
|
+
...(guard?.args ?? []),
|
|
93
|
+
...(args.inspect === true ? ['--inspect'] : []),
|
|
94
|
+
])
|
|
95
|
+
const vite = await cli.services.loadVite(root)
|
|
96
|
+
const tracker = createOutputTracker(root)
|
|
97
|
+
let entry: string | undefined
|
|
98
|
+
|
|
99
|
+
const supervisor = new AppSupervisor({
|
|
100
|
+
debounceMs,
|
|
101
|
+
killTimeoutMs,
|
|
102
|
+
spawn: () => {
|
|
103
|
+
const file = entry ?? resolve(root, 'dist/main.js')
|
|
104
|
+
return cli.services.processes.spawn(executable, [...runtimeArgs, file, ...passthrough], {
|
|
105
|
+
cwd: root,
|
|
106
|
+
env: { ...env, ...guard?.env, NODE_ENV: env['NODE_ENV'] ?? 'development' },
|
|
107
|
+
detached: true,
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
events: {
|
|
111
|
+
onStart: (child, restart) => {
|
|
112
|
+
const file = displayPath(entry ?? resolve(root, 'dist/main.js'), cli.cwd)
|
|
113
|
+
reporter.info(
|
|
114
|
+
`${restart ? 'Restarted' : 'Started'} ${colors.bold(file)} ${colors.dim(`(${runtime}, pid ${child.pid ?? '?'})`)}`,
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
onStop: (_status, forced) => {
|
|
118
|
+
if (forced) reporter.warn(`The application did not stop within ${killTimeoutMs}ms: killed`)
|
|
119
|
+
},
|
|
120
|
+
onExit: status => {
|
|
121
|
+
if (status.error !== undefined) {
|
|
122
|
+
reporter.error(`Cannot start ${runtime}: ${status.error.message}`)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
const code = exitCodeOf(status)
|
|
126
|
+
const message = `The application exited with code ${code}${status.signal === null ? '' : ` (${status.signal})`}`
|
|
127
|
+
if (code === 0) reporter.info(`${message}. Waiting for changes...`)
|
|
128
|
+
else reporter.error(`${message}. Waiting for changes...`)
|
|
129
|
+
},
|
|
130
|
+
onError: error =>
|
|
131
|
+
reporter.error(`Cannot start the application: ${error instanceof Error ? error.message : String(error)}`),
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const state = { failed: false, first: true, started: 0 }
|
|
136
|
+
const onEvent = (event: WatcherEvent): void => {
|
|
137
|
+
switch (event.code) {
|
|
138
|
+
case 'START':
|
|
139
|
+
state.failed = false
|
|
140
|
+
state.started = performance.now()
|
|
141
|
+
if (!state.first) {
|
|
142
|
+
if (args.clear === true && cli.stdout.isTTY === true) cli.stdout.write(CLEAR_SCREEN)
|
|
143
|
+
reporter.info(colors.dim('Change detected, rebuilding...'))
|
|
144
|
+
}
|
|
145
|
+
break
|
|
146
|
+
case 'ERROR':
|
|
147
|
+
state.failed = true
|
|
148
|
+
state.first = false
|
|
149
|
+
// Vite closes the build of BUNDLE_END itself, not the one of ERROR
|
|
150
|
+
void Promise.resolve()
|
|
151
|
+
.then(() => event.result?.close())
|
|
152
|
+
.catch(() => undefined)
|
|
153
|
+
// Vite logs build errors itself, unless it is silenced
|
|
154
|
+
if (logLevel === 'silent') reporter.logError(formatBuildError(event.error))
|
|
155
|
+
reporter.error(
|
|
156
|
+
`Build failed. ${supervisor.running ? 'The last good build keeps running. ' : ''}Waiting for changes...`,
|
|
157
|
+
)
|
|
158
|
+
break
|
|
159
|
+
case 'END': {
|
|
160
|
+
state.first = false
|
|
161
|
+
if (state.failed) break
|
|
162
|
+
// the bundle was written, but with a failed compile it throws the diagnostics at startup
|
|
163
|
+
const compile = tracker.compile()
|
|
164
|
+
if (compile !== undefined && !compile.ok) {
|
|
165
|
+
// a restart still pending for a previous build would run this broken output
|
|
166
|
+
supervisor.cancelRestart()
|
|
167
|
+
const errors = compile.diagnostics.filter(diagnostic => diagnostic.severity === 'error').length
|
|
168
|
+
// the znight() plugin prints the diagnostics through the Vite logger, unless it is silenced
|
|
169
|
+
const printed = logLevel === 'silent' ? printDiagnostics(cli, root, compile.diagnostics) : Promise.resolve()
|
|
170
|
+
void printed.then(() =>
|
|
171
|
+
reporter.error(
|
|
172
|
+
`Compilation failed with ${plural(errors, 'error')}. ${supervisor.running ? 'The last good build keeps running. ' : ''}Waiting for changes...`,
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
entry = args.entry === undefined ? (tracker.entry() ?? entry) : resolve(root, args.entry)
|
|
178
|
+
reporter.success(`Built in ${formatDuration(performance.now() - (tracker.startedAt() ?? state.started))}`)
|
|
179
|
+
supervisor.scheduleRestart()
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
default:
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
reporter.info(
|
|
188
|
+
`${colors.bold('znight dev')} ${colors.dim(`v${cli.version}`)} — vite build --watch, ${runtime} runtime`,
|
|
189
|
+
)
|
|
190
|
+
let watcher: BuildWatcher
|
|
191
|
+
try {
|
|
192
|
+
watcher = await watchBuild(vite, {
|
|
193
|
+
root,
|
|
194
|
+
configFile: args.config,
|
|
195
|
+
configLoader: args['config-loader'] as ViteConfigLoader | undefined,
|
|
196
|
+
mode: args.mode,
|
|
197
|
+
logLevel,
|
|
198
|
+
plugins: [tracker.plugin],
|
|
199
|
+
})
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error instanceof CliError) throw error
|
|
202
|
+
// configuration errors (the config file does not load, invalid options...)
|
|
203
|
+
reporter.logError(formatBuildError(error))
|
|
204
|
+
throw new CliError('Cannot start the Vite build', { cause: error })
|
|
205
|
+
}
|
|
206
|
+
watcher.on('event', onEvent)
|
|
207
|
+
return waitForTermination(cli, watcher, supervisor)
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
/** Prints compiler diagnostics with the project's `formatDiagnostics()` (a built-in formatter otherwise). */
|
|
212
|
+
async function printDiagnostics(cli: CliContext, root: string, diagnostics: readonly Diagnostic[]): Promise<void> {
|
|
213
|
+
if (diagnostics.length === 0) return
|
|
214
|
+
const options = { color: cli.colors.isColorSupported, cwd: root, summary: false }
|
|
215
|
+
let text: string
|
|
216
|
+
try {
|
|
217
|
+
text = (await cli.services.loadCompiler(root)).formatDiagnostics(diagnostics, options)
|
|
218
|
+
} catch {
|
|
219
|
+
text = fallbackFormatDiagnostics(diagnostics, options)
|
|
220
|
+
}
|
|
221
|
+
cli.reporter.logError(text.trimEnd())
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function waitForTermination(cli: CliContext, watcher: BuildWatcher, supervisor: AppSupervisor): Promise<number> {
|
|
225
|
+
return new Promise<number>(resolveExit => {
|
|
226
|
+
const dispose = onTermination(
|
|
227
|
+
cli,
|
|
228
|
+
signal => {
|
|
229
|
+
// SIGHUP: the terminal is gone, nothing can be printed
|
|
230
|
+
if (signal !== 'SIGHUP') cli.reporter.info(cli.colors.dim('Stopping... (press Ctrl+C again to force)'))
|
|
231
|
+
void (async () => {
|
|
232
|
+
try {
|
|
233
|
+
await watcher.close()
|
|
234
|
+
} finally {
|
|
235
|
+
// the application stops gracefully on SIGINT or SIGTERM
|
|
236
|
+
await supervisor.stop(signal === 'SIGINT' ? 'SIGINT' : 'SIGTERM')
|
|
237
|
+
dispose()
|
|
238
|
+
resolveExit(0)
|
|
239
|
+
}
|
|
240
|
+
})()
|
|
241
|
+
},
|
|
242
|
+
() => {
|
|
243
|
+
supervisor.kill()
|
|
244
|
+
dispose()
|
|
245
|
+
resolveExit(130)
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `znight generate`: writes the DI container module (`src/.znight/container.generated.ts`) with the
|
|
3
|
+
* compiler, checks that it is up to date (`--check`, for CI) or keeps it up to date (`--watch`, for
|
|
4
|
+
* `tsc --watch` and editors).
|
|
5
|
+
*/
|
|
6
|
+
import type { Command } from '../command.ts'
|
|
7
|
+
import type { CliContext } from '../context.ts'
|
|
8
|
+
import type {
|
|
9
|
+
CheckResultLike,
|
|
10
|
+
CompileResultLike,
|
|
11
|
+
CompilerApi,
|
|
12
|
+
GenerateResultLike,
|
|
13
|
+
IncrementalCompiler,
|
|
14
|
+
ZnightConfigLike,
|
|
15
|
+
ZnightOptions,
|
|
16
|
+
} from '../integrations/compiler.ts'
|
|
17
|
+
|
|
18
|
+
import { existsSync } from 'node:fs'
|
|
19
|
+
import { resolve } from 'node:path'
|
|
20
|
+
|
|
21
|
+
import { defineCommand } from '../command.ts'
|
|
22
|
+
import { CliError, errorMessage } from '../errors.ts'
|
|
23
|
+
import { hasErrors } from '../integrations/compiler.ts'
|
|
24
|
+
import { findViteConfig, readZnightPluginOptions, type ViteConfigLoader } from '../integrations/vite.ts'
|
|
25
|
+
import {
|
|
26
|
+
changeKind,
|
|
27
|
+
classifyChanges,
|
|
28
|
+
realPath,
|
|
29
|
+
TargetWatchers,
|
|
30
|
+
toPosixPath,
|
|
31
|
+
watchTargets,
|
|
32
|
+
} from '../integrations/watch.ts'
|
|
33
|
+
import { formatDuration, plural } from '../output.ts'
|
|
34
|
+
import { displayPath, onTermination, resolveRoot } from './shared.ts'
|
|
35
|
+
|
|
36
|
+
/** `znight generate` */
|
|
37
|
+
export const generateCommand: Command = defineCommand({
|
|
38
|
+
meta: {
|
|
39
|
+
name: 'generate',
|
|
40
|
+
description: 'Generate the dependency injection container (src/.znight/container.generated.ts)',
|
|
41
|
+
usage: '[options]',
|
|
42
|
+
examples: ['znight generate', 'znight generate --check # in CI, before tsc', 'znight generate --watch'],
|
|
43
|
+
details: [
|
|
44
|
+
'The compiler options are read from znight.config.* (or the file named by ZNIGHT_CONFIG), the file',
|
|
45
|
+
'shared with the Bun preload; without one, from the znight() plugin of vite.config.ts, so that the',
|
|
46
|
+
'generated file is the same as the one of the Vite build. --config reads the given Vite config.',
|
|
47
|
+
'"znight generate <schematic> <name>" is an alias of "znight g <schematic> <name>".',
|
|
48
|
+
].join('\n'),
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
check: {
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
description: 'Do not write: exit with code 1 when the generated file is missing or out of date',
|
|
54
|
+
},
|
|
55
|
+
watch: { type: 'boolean', alias: 'w', description: 'Regenerate on every change' },
|
|
56
|
+
root: { type: 'string', valueHint: 'dir', description: 'Project root (default: the current directory)' },
|
|
57
|
+
config: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
valueHint: 'file',
|
|
60
|
+
description: 'Vite config file to read the znight() options from (instead of znight.config.*)',
|
|
61
|
+
},
|
|
62
|
+
'config-loader': {
|
|
63
|
+
type: 'enum',
|
|
64
|
+
options: ['bundle', 'runner', 'native'],
|
|
65
|
+
description: 'How Vite loads the config file (default: bundle)',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
async run({ args, cli }) {
|
|
69
|
+
const root = resolveRoot(cli, args.root)
|
|
70
|
+
const compiler = await cli.services.loadCompiler(root)
|
|
71
|
+
const configLoader = args['config-loader'] as ViteConfigLoader | undefined
|
|
72
|
+
const options = await compilerOptions(cli, compiler, root, args.config, configLoader)
|
|
73
|
+
if (args.check === true) return check(cli, compiler, options)
|
|
74
|
+
if (args.watch === true) return watch(cli, compiler, options)
|
|
75
|
+
const result = await generateOnce(cli, compiler, options)
|
|
76
|
+
return hasErrors(result.diagnostics) ? 1 : 0
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The compiler options, with `root`: those of `znight.config.*` when the project has one (unless a
|
|
82
|
+
* Vite config is given with `--config`), else those of `znight()` in the Vite config (when exposed),
|
|
83
|
+
* else the defaults.
|
|
84
|
+
*/
|
|
85
|
+
export async function compilerOptions(
|
|
86
|
+
cli: CliContext,
|
|
87
|
+
compiler: CompilerApi,
|
|
88
|
+
root: string,
|
|
89
|
+
configFile: string | undefined,
|
|
90
|
+
configLoader: ViteConfigLoader | undefined,
|
|
91
|
+
): Promise<ZnightOptions> {
|
|
92
|
+
if (configFile === undefined) {
|
|
93
|
+
const shared = await znightConfigOptions(compiler, root)
|
|
94
|
+
if (shared !== undefined) return shared
|
|
95
|
+
}
|
|
96
|
+
let options: ZnightOptions | undefined
|
|
97
|
+
// no Vite config: nothing to read, do not even load Vite
|
|
98
|
+
if (findViteConfig(root, configFile) === undefined) return { root }
|
|
99
|
+
try {
|
|
100
|
+
const vite = await cli.services.loadVite(root)
|
|
101
|
+
const lookup = await readZnightPluginOptions(vite, root, configFile, configLoader)
|
|
102
|
+
if (lookup !== undefined && !lookup.found) {
|
|
103
|
+
cli.reporter.warn(`${displayPath(lookup.configFile, cli.cwd)} does not register the znight() plugin`)
|
|
104
|
+
}
|
|
105
|
+
options = lookup?.options
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (configFile !== undefined) {
|
|
108
|
+
throw new CliError(`Cannot load the Vite config ${configFile}`, { cause: error, hints: [errorMessage(error)] })
|
|
109
|
+
}
|
|
110
|
+
cli.reporter.warn(
|
|
111
|
+
`Cannot read the znight() options from the Vite config, using the defaults: ${errorMessage(error)}`,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
return { ...options, root: resolve(root, options?.root ?? '.') }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Options of `znight.config.*` (`loadZnightConfig()` of the compiler), `undefined` without the file. */
|
|
118
|
+
async function znightConfigOptions(compiler: CompilerApi, root: string): Promise<ZnightOptions | undefined> {
|
|
119
|
+
if (compiler.loadConfig === undefined) return undefined
|
|
120
|
+
let config: ZnightConfigLike
|
|
121
|
+
try {
|
|
122
|
+
config = await compiler.loadConfig(root)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw new CliError('Cannot load the Znight config (znight.config.*)', {
|
|
125
|
+
cause: error,
|
|
126
|
+
hints: [errorMessage(error)],
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
if (config.file === undefined) return undefined
|
|
130
|
+
// `logLevel` is an option of the Bun preload only
|
|
131
|
+
const { logLevel: _preloadLogLevel, ...options } = config.options
|
|
132
|
+
return { ...options, root: resolve(root, options.root ?? '.') }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function printDiagnostics(cli: CliContext, compiler: CompilerApi, result: CompileResultLike, root: string): void {
|
|
136
|
+
if (result.diagnostics.length === 0) return
|
|
137
|
+
const text = compiler.formatDiagnostics(result.diagnostics, {
|
|
138
|
+
color: cli.colors.isColorSupported,
|
|
139
|
+
cwd: root,
|
|
140
|
+
summary: false,
|
|
141
|
+
})
|
|
142
|
+
cli.reporter.logError(text.trimEnd())
|
|
143
|
+
cli.reporter.logError()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function statsOf(result: CompileResultLike, elapsedMs: number): string {
|
|
147
|
+
const parts: string[] = []
|
|
148
|
+
if (result.stats?.providers !== undefined) parts.push(plural(result.stats.providers, 'provider'))
|
|
149
|
+
if (result.stats?.files !== undefined) parts.push(plural(result.stats.files, 'file'))
|
|
150
|
+
parts.push(formatDuration(result.stats?.durationMs ?? elapsedMs))
|
|
151
|
+
return parts.join(', ')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function failure(cli: CliContext, result: CompileResultLike): void {
|
|
155
|
+
const errors = result.diagnostics.filter(diagnostic => diagnostic.severity === 'error').length
|
|
156
|
+
cli.reporter.error(`Generation failed with ${plural(errors, 'error')}`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Prints the outcome of a generation. */
|
|
160
|
+
function report(
|
|
161
|
+
cli: CliContext,
|
|
162
|
+
compiler: CompilerApi,
|
|
163
|
+
result: GenerateResultLike,
|
|
164
|
+
root: string,
|
|
165
|
+
elapsed: number,
|
|
166
|
+
): void {
|
|
167
|
+
printDiagnostics(cli, compiler, result, root)
|
|
168
|
+
if (hasErrors(result.diagnostics)) {
|
|
169
|
+
failure(cli, result)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
const file = displayPath(result.outFile, cli.cwd)
|
|
173
|
+
cli.reporter.success(
|
|
174
|
+
`${result.written ? 'Generated' : 'Up to date:'} ${cli.colors.bold(file)} ${cli.colors.dim(`(${statsOf(result, elapsed)})`)}`,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function generateOnce(
|
|
179
|
+
cli: CliContext,
|
|
180
|
+
compiler: CompilerApi,
|
|
181
|
+
options: ZnightOptions,
|
|
182
|
+
): Promise<GenerateResultLike> {
|
|
183
|
+
const started = performance.now()
|
|
184
|
+
const result = await compiler.generate(options)
|
|
185
|
+
report(cli, compiler, result, options.root ?? cli.cwd, performance.now() - started)
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The generated files `--check` reports, the module first: those the compiler says are stale (the
|
|
191
|
+
* module when it does not say which), and the module when it is missing.
|
|
192
|
+
*/
|
|
193
|
+
function staleFilesOf(result: CheckResultLike): string[] {
|
|
194
|
+
const stale = new Set<string>()
|
|
195
|
+
if (!existsSync(result.outFile)) stale.add(result.outFile)
|
|
196
|
+
if (result.stale) {
|
|
197
|
+
const files = result.staleFiles ?? []
|
|
198
|
+
for (const file of files.length > 0 ? files : [result.outFile]) stale.add(file)
|
|
199
|
+
}
|
|
200
|
+
return [...stale].sort((a, b) => Number(b === result.outFile) - Number(a === result.outFile))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function check(cli: CliContext, compiler: CompilerApi, options: ZnightOptions): Promise<number> {
|
|
204
|
+
const root = options.root ?? cli.cwd
|
|
205
|
+
const started = performance.now()
|
|
206
|
+
const result = await compiler.check(options)
|
|
207
|
+
printDiagnostics(cli, compiler, result, root)
|
|
208
|
+
if (hasErrors(result.diagnostics)) {
|
|
209
|
+
failure(cli, result)
|
|
210
|
+
return 1
|
|
211
|
+
}
|
|
212
|
+
const file = displayPath(result.outFile, cli.cwd)
|
|
213
|
+
const stale = staleFilesOf(result)
|
|
214
|
+
for (const path of stale) {
|
|
215
|
+
const shown = displayPath(path, cli.cwd)
|
|
216
|
+
// the other generated file is the library manifest (`dist/znight.providers.json`)
|
|
217
|
+
const hint =
|
|
218
|
+
path === result.outFile
|
|
219
|
+
? 'run "znight generate" (it is git-ignored: generate it before tsc and tests)'
|
|
220
|
+
: 'run "znight generate" after the build: a build cleaning dist/ (tsdown, tsc --build --clean...) deletes it'
|
|
221
|
+
if (existsSync(path)) cli.reporter.error(`${shown} is out of date`, ['run "znight generate"'])
|
|
222
|
+
else cli.reporter.error(`${shown} is missing`, [hint])
|
|
223
|
+
}
|
|
224
|
+
if (stale.length > 0) return 1
|
|
225
|
+
cli.reporter.success(
|
|
226
|
+
`${cli.colors.bold(file)} is up to date ${cli.colors.dim(`(${statsOf(result, performance.now() - started)})`)}`,
|
|
227
|
+
)
|
|
228
|
+
return 0
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** What the watcher knows about the last compile. */
|
|
232
|
+
interface WatchState {
|
|
233
|
+
/** Files the last compile depends on (`watchFiles`): absolute, real, POSIX-style. */
|
|
234
|
+
readonly known: ReadonlySet<string>
|
|
235
|
+
/** Directories where a created or deleted source changes the scan (`includeRoots()`). */
|
|
236
|
+
readonly roots: readonly string[]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function watch(cli: CliContext, compiler: CompilerApi, options: ZnightOptions): Promise<number> {
|
|
240
|
+
const root = options.root ?? cli.cwd
|
|
241
|
+
const realRoot = realPath(root)
|
|
242
|
+
const incremental: IncrementalCompiler = compiler.createIncremental(options)
|
|
243
|
+
let state: WatchState = { known: new Set(), roots: [] }
|
|
244
|
+
let pending = new Set<string>()
|
|
245
|
+
let running = false
|
|
246
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
247
|
+
|
|
248
|
+
// which events matter is the compiler's call: the CLI only tells their kind, which fs.watch does not
|
|
249
|
+
const watchers = new TargetWatchers(cli.services.watch, file => {
|
|
250
|
+
const path = realPath(file)
|
|
251
|
+
if (!incremental.isRelevantChange(changeKind(path, state.known), path, state.known, state.roots)) return
|
|
252
|
+
pending.add(path)
|
|
253
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
254
|
+
timer = setTimeout(() => {
|
|
255
|
+
timer = undefined
|
|
256
|
+
void run(false)
|
|
257
|
+
}, 50)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const run = async (initial: boolean): Promise<void> => {
|
|
261
|
+
if (running) return
|
|
262
|
+
running = true
|
|
263
|
+
let first = initial
|
|
264
|
+
try {
|
|
265
|
+
do {
|
|
266
|
+
const files = pending
|
|
267
|
+
pending = new Set()
|
|
268
|
+
const started = performance.now()
|
|
269
|
+
let known = state.known
|
|
270
|
+
try {
|
|
271
|
+
const result = await incremental.generate(first ? undefined : classifyChanges(files, state.known))
|
|
272
|
+
if (result.watchFiles !== undefined) {
|
|
273
|
+
known = new Set(result.watchFiles.map(file => toPosixPath(resolve(file))))
|
|
274
|
+
}
|
|
275
|
+
report(cli, compiler, result, root, performance.now() - started)
|
|
276
|
+
} catch (error) {
|
|
277
|
+
cli.reporter.error(`Generation failed: ${errorMessage(error)}`)
|
|
278
|
+
}
|
|
279
|
+
// the include directories of the source libraries are known once compiled
|
|
280
|
+
state = { known, roots: incremental.includeRoots().map(realPath) }
|
|
281
|
+
watchers.update(watchTargets(realRoot, state.roots, state.known), files)
|
|
282
|
+
first = false
|
|
283
|
+
} while (pending.size > 0)
|
|
284
|
+
} finally {
|
|
285
|
+
running = false
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await run(true)
|
|
290
|
+
cli.reporter.info(cli.colors.dim('Watching for changes... (Ctrl+C to stop)'))
|
|
291
|
+
|
|
292
|
+
return new Promise<number>(resolveExit => {
|
|
293
|
+
const dispose = onTermination(
|
|
294
|
+
cli,
|
|
295
|
+
() => {
|
|
296
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
297
|
+
watchers.close()
|
|
298
|
+
dispose()
|
|
299
|
+
resolveExit(0)
|
|
300
|
+
},
|
|
301
|
+
() => undefined,
|
|
302
|
+
)
|
|
303
|
+
})
|
|
304
|
+
}
|