@znight/cli 0.0.0-stage → 0.1.1
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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts of `znight new`, backed by `@clack/prompts` (loaded lazily). A cancelled
|
|
3
|
+
* prompt (Ctrl+C, Escape) throws a {@link CancelledError}.
|
|
4
|
+
*/
|
|
5
|
+
import { CancelledError } from '../errors.ts'
|
|
6
|
+
|
|
7
|
+
/** A choice of a select / multiselect prompt. */
|
|
8
|
+
export interface PromptChoice<T extends string> {
|
|
9
|
+
readonly value: T
|
|
10
|
+
readonly label: string
|
|
11
|
+
readonly hint?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Prompts used by the CLI. */
|
|
15
|
+
export interface Prompts {
|
|
16
|
+
intro(title: string): void
|
|
17
|
+
outro(message: string): void
|
|
18
|
+
text(options: {
|
|
19
|
+
message: string
|
|
20
|
+
placeholder?: string
|
|
21
|
+
initialValue?: string
|
|
22
|
+
validate?: (value: string) => string | undefined
|
|
23
|
+
}): Promise<string>
|
|
24
|
+
select<T extends string>(options: {
|
|
25
|
+
message: string
|
|
26
|
+
choices: readonly PromptChoice<T>[]
|
|
27
|
+
initialValue?: T
|
|
28
|
+
}): Promise<T>
|
|
29
|
+
multiselect<T extends string>(options: {
|
|
30
|
+
message: string
|
|
31
|
+
choices: readonly PromptChoice<T>[]
|
|
32
|
+
initialValues?: readonly T[]
|
|
33
|
+
}): Promise<T[]>
|
|
34
|
+
confirm(options: { message: string; initialValue?: boolean }): Promise<boolean>
|
|
35
|
+
/** A spinner; `stop` prints the final message. */
|
|
36
|
+
spinner(): { start(message: string): void; stop(message: string, failed?: boolean): void }
|
|
37
|
+
note(message: string, title?: string): void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Loads `@clack/prompts` and adapts it to {@link Prompts}. */
|
|
41
|
+
export async function loadPrompts(): Promise<Prompts> {
|
|
42
|
+
const clack = await import('@clack/prompts')
|
|
43
|
+
const unwrap = <T>(value: T): Exclude<T, symbol> => {
|
|
44
|
+
if (clack.isCancel(value) || typeof value === 'symbol') throw new CancelledError('Operation cancelled')
|
|
45
|
+
return value as Exclude<T, symbol>
|
|
46
|
+
}
|
|
47
|
+
const toOptions = (choices: readonly PromptChoice<string>[]): { value: string; label: string; hint?: string }[] =>
|
|
48
|
+
choices.map(choice => ({
|
|
49
|
+
value: choice.value,
|
|
50
|
+
label: choice.label,
|
|
51
|
+
...(choice.hint === undefined ? {} : { hint: choice.hint }),
|
|
52
|
+
}))
|
|
53
|
+
return {
|
|
54
|
+
intro: title => clack.intro(title),
|
|
55
|
+
outro: message => clack.outro(message),
|
|
56
|
+
text: async options =>
|
|
57
|
+
unwrap(
|
|
58
|
+
await clack.text({
|
|
59
|
+
message: options.message,
|
|
60
|
+
...(options.placeholder === undefined ? {} : { placeholder: options.placeholder }),
|
|
61
|
+
...(options.initialValue === undefined ? {} : { initialValue: options.initialValue }),
|
|
62
|
+
...(options.validate === undefined
|
|
63
|
+
? {}
|
|
64
|
+
: { validate: (value: string | undefined) => options.validate?.(value ?? '') }),
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
select: async <T extends string>(options: {
|
|
68
|
+
message: string
|
|
69
|
+
choices: readonly PromptChoice<T>[]
|
|
70
|
+
initialValue?: T
|
|
71
|
+
}): Promise<T> => {
|
|
72
|
+
const value = await clack.select<string>({
|
|
73
|
+
message: options.message,
|
|
74
|
+
options: toOptions(options.choices),
|
|
75
|
+
...(options.initialValue === undefined ? {} : { initialValue: options.initialValue }),
|
|
76
|
+
})
|
|
77
|
+
return unwrap(value) as T
|
|
78
|
+
},
|
|
79
|
+
multiselect: async <T extends string>(options: {
|
|
80
|
+
message: string
|
|
81
|
+
choices: readonly PromptChoice<T>[]
|
|
82
|
+
initialValues?: readonly T[]
|
|
83
|
+
}): Promise<T[]> => {
|
|
84
|
+
const value = await clack.multiselect<string>({
|
|
85
|
+
message: options.message,
|
|
86
|
+
required: false,
|
|
87
|
+
options: toOptions(options.choices),
|
|
88
|
+
...(options.initialValues === undefined ? {} : { initialValues: [...options.initialValues] }),
|
|
89
|
+
})
|
|
90
|
+
return unwrap(value) as T[]
|
|
91
|
+
},
|
|
92
|
+
confirm: async options =>
|
|
93
|
+
unwrap(
|
|
94
|
+
await clack.confirm({
|
|
95
|
+
message: options.message,
|
|
96
|
+
...(options.initialValue === undefined ? {} : { initialValue: options.initialValue }),
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
spinner: () => {
|
|
100
|
+
const spinner = clack.spinner()
|
|
101
|
+
return {
|
|
102
|
+
start: message => spinner.start(message),
|
|
103
|
+
stop: (message, failed = false) => (failed ? spinner.error(message) : spinner.stop(message)),
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
note: (message, title) => clack.note(message, title),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Imports `specifier` as resolved from the project `root` (the versions the project installed, like
|
|
7
|
+
* `vite` and `tsc` do), or with `fallback` (resolved from the CLI) when the project does not have it.
|
|
8
|
+
*/
|
|
9
|
+
export async function importFromProject(
|
|
10
|
+
specifier: string,
|
|
11
|
+
root: string,
|
|
12
|
+
fallback: () => Promise<unknown>,
|
|
13
|
+
): Promise<unknown> {
|
|
14
|
+
const resolved = resolveFromProject(specifier, root)
|
|
15
|
+
if (resolved !== undefined) {
|
|
16
|
+
try {
|
|
17
|
+
return await import(pathToFileURL(resolved).href)
|
|
18
|
+
} catch (error) {
|
|
19
|
+
// a broken project install must not hide behind the fallback
|
|
20
|
+
if (!isModuleNotFound(error)) throw error
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return fallback()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Absolute path of `specifier` resolved from `root`, or `undefined`. */
|
|
27
|
+
export function resolveFromProject(specifier: string, root: string): string | undefined {
|
|
28
|
+
try {
|
|
29
|
+
return createRequire(join(root, 'package.json')).resolve(specifier)
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isModuleNotFound(error: unknown): boolean {
|
|
36
|
+
const code = (error as { code?: unknown } | null)?.code
|
|
37
|
+
return code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND'
|
|
38
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite, loaded lazily from the project (it is a peer dependency). `znight build` and `znight dev` run
|
|
3
|
+
* `vite build` programmatically: the `znight()` plugin of the project's `vite.config.ts` compiles the
|
|
4
|
+
* container and, with its `app` defaults, configures a server build (`src/main.ts` → `dist/`).
|
|
5
|
+
*
|
|
6
|
+
* The Vite root is resolved like the Vite CLI run in the project directory: the `root` of the config
|
|
7
|
+
* file, relative to the project root (`--root`), else the project root ({@link projectRootPlugin}).
|
|
8
|
+
*/
|
|
9
|
+
import type { InlineConfig, Plugin } from 'vite'
|
|
10
|
+
import type { Diagnostic, StringAliasLike, ZnightOptions } from './compiler.ts'
|
|
11
|
+
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import { join, resolve } from 'node:path'
|
|
14
|
+
|
|
15
|
+
import { CliError } from '../errors.ts'
|
|
16
|
+
import { importFromProject } from './resolve.ts'
|
|
17
|
+
|
|
18
|
+
/** Vite log levels. */
|
|
19
|
+
export type ViteLogLevel = 'info' | 'warn' | 'error' | 'silent'
|
|
20
|
+
|
|
21
|
+
/** How Vite loads its config file (`--configLoader` of the Vite CLI). */
|
|
22
|
+
export type ViteConfigLoader = 'bundle' | 'runner' | 'native'
|
|
23
|
+
|
|
24
|
+
/** What the CLI reads from a config resolved by Vite (`ResolvedConfig`). */
|
|
25
|
+
export interface ResolvedConfigLike {
|
|
26
|
+
/** Absolute root. */
|
|
27
|
+
readonly root: string
|
|
28
|
+
/** The config file that was loaded. */
|
|
29
|
+
readonly configFile?: string | undefined
|
|
30
|
+
/** Every plugin, flattened. */
|
|
31
|
+
readonly plugins: readonly unknown[]
|
|
32
|
+
readonly resolve?: { readonly alias?: unknown } | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Subset of the `vite` module used by the CLI. */
|
|
36
|
+
export interface ViteApi {
|
|
37
|
+
build(config: InlineConfig): Promise<unknown>
|
|
38
|
+
/** Resolves the config as a build would, without building (absent: `znight generate` uses defaults). */
|
|
39
|
+
resolveConfig?: (
|
|
40
|
+
config: InlineConfig,
|
|
41
|
+
command: 'build' | 'serve',
|
|
42
|
+
defaultMode?: string,
|
|
43
|
+
defaultNodeEnv?: string,
|
|
44
|
+
) => Promise<ResolvedConfigLike>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Events of a Vite build in watch mode (Rolldown/Rollup watcher events). */
|
|
48
|
+
export type WatcherEvent =
|
|
49
|
+
| { readonly code: 'START' }
|
|
50
|
+
| { readonly code: 'BUNDLE_START' }
|
|
51
|
+
| { readonly code: 'BUNDLE_END'; readonly duration?: number; readonly result?: ClosableBuild }
|
|
52
|
+
| { readonly code: 'END' }
|
|
53
|
+
| { readonly code: 'ERROR'; readonly error: unknown; readonly result?: ClosableBuild }
|
|
54
|
+
|
|
55
|
+
interface ClosableBuild {
|
|
56
|
+
close(): unknown
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The watcher returned by `vite.build({ build: { watch } })`. */
|
|
60
|
+
export interface BuildWatcher {
|
|
61
|
+
on(event: 'event', listener: (event: WatcherEvent) => void): unknown
|
|
62
|
+
close(): Promise<void>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Loads `vite` as installed in the project. */
|
|
66
|
+
export async function loadVite(root: string): Promise<ViteApi> {
|
|
67
|
+
try {
|
|
68
|
+
return (await importFromProject('vite', root, () => import('vite'))) as ViteApi
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new CliError('Cannot load vite', {
|
|
71
|
+
cause: error,
|
|
72
|
+
hints: ['install it in your project: npm install -D vite @znight/compiler'],
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Options shared by {@link runBuild} and {@link watchBuild}. */
|
|
78
|
+
export interface BuildOptions {
|
|
79
|
+
readonly root: string
|
|
80
|
+
/** Vite config file; default: auto-detected `vite.config.*`. */
|
|
81
|
+
readonly configFile?: string | undefined
|
|
82
|
+
readonly mode: string
|
|
83
|
+
readonly logLevel: ViteLogLevel
|
|
84
|
+
/** How the config file is loaded (default: Vite's, `bundle`). */
|
|
85
|
+
readonly configLoader?: ViteConfigLoader | undefined
|
|
86
|
+
/** Plugins added to the config file's ones. */
|
|
87
|
+
readonly plugins?: readonly Plugin[]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The outcome of the compile of the `znight()` plugin (`plugin.api.result`, @znight/compiler). */
|
|
91
|
+
export interface CompileOutcome {
|
|
92
|
+
/** No error diagnostic. */
|
|
93
|
+
readonly ok: boolean
|
|
94
|
+
readonly diagnostics: readonly Diagnostic[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** What the CLI reads from the `plugin.api` of `znight()` (structural: any compiler version). */
|
|
98
|
+
interface ZnightPluginApiLike {
|
|
99
|
+
/** The latest compile result. */
|
|
100
|
+
readonly result?: unknown
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Captures what a build produced: the entry chunk written (the file `znight dev` runs) and the
|
|
105
|
+
* compile result of the `znight()` plugin. In watch mode the plugin does not fail the build on
|
|
106
|
+
* compile errors (the bundle contains a module throwing them): `znight dev` reads the result to keep
|
|
107
|
+
* the last good process running.
|
|
108
|
+
*/
|
|
109
|
+
export interface OutputTracker {
|
|
110
|
+
readonly plugin: Plugin
|
|
111
|
+
/** Absolute path of the entry chunk written by the last build. */
|
|
112
|
+
entry(): string | undefined
|
|
113
|
+
/** `performance.now()` when the last build started (`undefined` before the first one). */
|
|
114
|
+
startedAt(): number | undefined
|
|
115
|
+
/**
|
|
116
|
+
* The compile result the last written build was made of (`plugin.api.result` of `znight()`),
|
|
117
|
+
* `undefined` when the Vite config has no `znight()` plugin exposing it.
|
|
118
|
+
*/
|
|
119
|
+
compile(): CompileOutcome | undefined
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Creates an {@link OutputTracker}; relative output paths are resolved against the Vite root. */
|
|
123
|
+
export function createOutputTracker(root: string): OutputTracker {
|
|
124
|
+
let entry: string | undefined
|
|
125
|
+
let started: number | undefined
|
|
126
|
+
let api: ZnightPluginApiLike | undefined
|
|
127
|
+
let compile: CompileOutcome | undefined
|
|
128
|
+
let viteRoot = root
|
|
129
|
+
const plugin: Plugin = {
|
|
130
|
+
name: 'znight:cli-output',
|
|
131
|
+
configResolved(config) {
|
|
132
|
+
api = findZnightApi(config.plugins)
|
|
133
|
+
if (typeof config.root === 'string') viteRoot = config.root
|
|
134
|
+
},
|
|
135
|
+
buildStart() {
|
|
136
|
+
started = performance.now()
|
|
137
|
+
compile = undefined
|
|
138
|
+
},
|
|
139
|
+
writeBundle(outputOptions, bundle) {
|
|
140
|
+
// the plugin compiled in its buildStart: its latest result is the one this bundle contains
|
|
141
|
+
compile = compileOutcome(api?.result)
|
|
142
|
+
const chunks = Object.values(bundle).filter(
|
|
143
|
+
(output): output is Extract<(typeof bundle)[string], { type: 'chunk' }> =>
|
|
144
|
+
output.type === 'chunk' && output.isEntry,
|
|
145
|
+
)
|
|
146
|
+
const chunk = chunks.find(candidate => candidate.name === 'main') ?? chunks[0]
|
|
147
|
+
if (chunk === undefined) return
|
|
148
|
+
entry =
|
|
149
|
+
outputOptions.file === undefined
|
|
150
|
+
? resolve(viteRoot, outputOptions.dir ?? 'dist', chunk.fileName)
|
|
151
|
+
: resolve(viteRoot, outputOptions.file)
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
return { plugin, entry: () => entry, startedAt: () => started, compile: () => compile }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The `plugin.api` of the `znight()` plugin among the resolved plugins. */
|
|
158
|
+
function findZnightApi(plugins: readonly unknown[]): ZnightPluginApiLike | undefined {
|
|
159
|
+
for (const plugin of plugins) {
|
|
160
|
+
if (typeof plugin !== 'object' || plugin === null || (plugin as { name?: unknown }).name !== 'znight') continue
|
|
161
|
+
const api = (plugin as { api?: unknown }).api
|
|
162
|
+
if (typeof api === 'object' && api !== null && 'result' in api) return api as ZnightPluginApiLike
|
|
163
|
+
}
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function compileOutcome(result: unknown): CompileOutcome | undefined {
|
|
168
|
+
if (typeof result !== 'object' || result === null) return undefined
|
|
169
|
+
const { ok, diagnostics } = result as { ok?: unknown; diagnostics?: unknown }
|
|
170
|
+
if (typeof ok !== 'boolean') return undefined
|
|
171
|
+
return { ok, diagnostics: Array.isArray(diagnostics) ? (diagnostics as Diagnostic[]) : [] }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Name of {@link projectRootPlugin}. */
|
|
175
|
+
const ROOT_PLUGIN = 'znight:cli-root'
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the Vite root like the Vite CLI run in the project directory: the `root` of the config
|
|
179
|
+
* file against `projectRoot`, or `projectRoot` without one. An inline `root` would override the
|
|
180
|
+
* config file's, so the CLI sets it from the first `config` hook instead.
|
|
181
|
+
*/
|
|
182
|
+
export function projectRootPlugin(projectRoot: string): Plugin {
|
|
183
|
+
return {
|
|
184
|
+
name: ROOT_PLUGIN,
|
|
185
|
+
config: {
|
|
186
|
+
order: 'pre',
|
|
187
|
+
handler: config => ({ root: resolve(projectRoot, config.root ?? '.') }),
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function inlineConfig(options: BuildOptions, watch: boolean): InlineConfig {
|
|
193
|
+
return {
|
|
194
|
+
// found in the project root (not the working directory); no config file: none is searched
|
|
195
|
+
configFile: findViteConfig(options.root, options.configFile) ?? false,
|
|
196
|
+
mode: options.mode,
|
|
197
|
+
logLevel: options.logLevel,
|
|
198
|
+
...(options.configLoader === undefined ? {} : { configLoader: options.configLoader }),
|
|
199
|
+
clearScreen: false,
|
|
200
|
+
plugins: [projectRootPlugin(options.root), ...(options.plugins ?? [])],
|
|
201
|
+
build: watch ? { watch: {} } : {},
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Runs `vite build` once. Throws the build error. */
|
|
206
|
+
export async function runBuild(vite: ViteApi, options: BuildOptions): Promise<void> {
|
|
207
|
+
const result = await vite.build(inlineConfig(options, false))
|
|
208
|
+
if (isWatcher(result)) await result.close()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Starts `vite build --watch`. */
|
|
212
|
+
export async function watchBuild(vite: ViteApi, options: BuildOptions): Promise<BuildWatcher> {
|
|
213
|
+
const result = await vite.build(inlineConfig(options, true))
|
|
214
|
+
if (!isWatcher(result)) {
|
|
215
|
+
throw new CliError('vite build did not start in watch mode', {
|
|
216
|
+
hints: ['check that your Vite config does not override "build.watch"'],
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
return result
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isWatcher(value: unknown): value is BuildWatcher {
|
|
223
|
+
return (
|
|
224
|
+
typeof value === 'object' &&
|
|
225
|
+
value !== null &&
|
|
226
|
+
typeof (value as Partial<BuildWatcher>).on === 'function' &&
|
|
227
|
+
typeof (value as Partial<BuildWatcher>).close === 'function'
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Vite's `DEFAULT_CONFIG_FILES`, in its order. */
|
|
232
|
+
const CONFIG_FILES = [
|
|
233
|
+
'vite.config.js',
|
|
234
|
+
'vite.config.mjs',
|
|
235
|
+
'vite.config.ts',
|
|
236
|
+
'vite.config.cjs',
|
|
237
|
+
'vite.config.mts',
|
|
238
|
+
'vite.config.cts',
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
/** The Vite config file of the project (the one Vite would load from `root`), if any. */
|
|
242
|
+
export function findViteConfig(root: string, configFile?: string): string | undefined {
|
|
243
|
+
if (configFile !== undefined) return resolve(root, configFile)
|
|
244
|
+
return CONFIG_FILES.map(file => join(root, file)).find(file => existsSync(file))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Result of {@link readZnightPluginOptions}. */
|
|
248
|
+
export interface ZnightPluginLookup {
|
|
249
|
+
/** The Vite config file that was loaded. */
|
|
250
|
+
readonly configFile: string
|
|
251
|
+
/** Whether the config registers the `znight()` plugin. */
|
|
252
|
+
readonly found: boolean
|
|
253
|
+
/**
|
|
254
|
+
* The options the `znight()` plugin compiles with: those given to it (`plugin.api.options`) with
|
|
255
|
+
* the absolute `root` (resolved against the Vite root) and, as derived from the Vite config, the
|
|
256
|
+
* string `aliases` of `resolve.alias` and the `runtimeConditions` (when the plugin exposes its
|
|
257
|
+
* compiler).
|
|
258
|
+
*/
|
|
259
|
+
readonly options?: ZnightOptions | undefined
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Resolves the project's Vite config as `znight build` does and reads the options the `znight()`
|
|
264
|
+
* plugin compiles with, so that `znight generate` compiles exactly like the Vite build. Returns
|
|
265
|
+
* `undefined` without a config file. `process.env.NODE_ENV`, which Vite sets, is left untouched.
|
|
266
|
+
*/
|
|
267
|
+
export async function readZnightPluginOptions(
|
|
268
|
+
vite: ViteApi,
|
|
269
|
+
root: string,
|
|
270
|
+
configFile?: string,
|
|
271
|
+
configLoader?: ViteConfigLoader,
|
|
272
|
+
): Promise<ZnightPluginLookup | undefined> {
|
|
273
|
+
const file = findViteConfig(root, configFile)
|
|
274
|
+
const resolveConfig = vite.resolveConfig
|
|
275
|
+
if (file === undefined || resolveConfig === undefined) return undefined
|
|
276
|
+
const inline: InlineConfig = {
|
|
277
|
+
configFile: file,
|
|
278
|
+
mode: 'production',
|
|
279
|
+
logLevel: 'silent',
|
|
280
|
+
...(configLoader === undefined ? {} : { configLoader }),
|
|
281
|
+
plugins: [projectRootPlugin(root)],
|
|
282
|
+
}
|
|
283
|
+
const config = await preservingNodeEnv(() => resolveConfig.call(vite, inline, 'build', 'production', 'production'))
|
|
284
|
+
const loaded = config.configFile ?? file
|
|
285
|
+
for (const plugin of config.plugins) {
|
|
286
|
+
if (typeof plugin !== 'object' || plugin === null) continue
|
|
287
|
+
const name = (plugin as { name?: unknown }).name
|
|
288
|
+
if (typeof name !== 'string' || !isZnightPluginName(name)) continue
|
|
289
|
+
return { configFile: loaded, found: true, options: compileOptionsOf(plugin, config) }
|
|
290
|
+
}
|
|
291
|
+
return { configFile: loaded, found: false }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** `znight` and its sub-plugins (`znight:*`), not the CLI's own plugins. */
|
|
295
|
+
function isZnightPluginName(name: string): boolean {
|
|
296
|
+
return name === 'znight' || (name.startsWith('znight:') && !name.startsWith('znight:cli-'))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function preservingNodeEnv<T>(run: () => Promise<T>): Promise<T> {
|
|
300
|
+
const nodeEnv = process.env['NODE_ENV']
|
|
301
|
+
try {
|
|
302
|
+
return await run()
|
|
303
|
+
} finally {
|
|
304
|
+
if (nodeEnv === undefined) delete process.env['NODE_ENV']
|
|
305
|
+
else process.env['NODE_ENV'] = nodeEnv
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null
|
|
310
|
+
|
|
311
|
+
/** {@link ZnightPluginLookup.options}: the compiler's resolved options when exposed, else derived. */
|
|
312
|
+
function compileOptionsOf(plugin: object, config: ResolvedConfigLike): ZnightOptions {
|
|
313
|
+
const api = (plugin as { api?: unknown }).api
|
|
314
|
+
let options: ZnightOptions = {}
|
|
315
|
+
let resolved: Record<string, unknown> | undefined
|
|
316
|
+
if (isObject(api)) {
|
|
317
|
+
const value: unknown = typeof api['getOptions'] === 'function' ? api['getOptions'].call(api) : api['options']
|
|
318
|
+
if (isObject(value)) options = value as ZnightOptions
|
|
319
|
+
const compiler = api['compiler']
|
|
320
|
+
if (isObject(compiler) && isObject(compiler['options'])) resolved = compiler['options']
|
|
321
|
+
}
|
|
322
|
+
const root = typeof resolved?.['root'] === 'string' ? resolved['root'] : resolve(config.root, options.root ?? '.')
|
|
323
|
+
const aliases = Array.isArray(resolved?.['aliases'])
|
|
324
|
+
? (resolved['aliases'] as StringAliasLike[])
|
|
325
|
+
: stringAliases(config.resolve?.alias)
|
|
326
|
+
const conditions = resolved?.['runtimeConditions']
|
|
327
|
+
return {
|
|
328
|
+
...options,
|
|
329
|
+
root,
|
|
330
|
+
...(aliases.length > 0 ? { aliases } : {}),
|
|
331
|
+
...(Array.isArray(conditions) ? { runtimeConditions: conditions as string[] } : {}),
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The string entries of Vite's (resolved) `resolve.alias`: the compiler ignores RegExp aliases. */
|
|
336
|
+
function stringAliases(alias: unknown): StringAliasLike[] {
|
|
337
|
+
if (!Array.isArray(alias)) return []
|
|
338
|
+
return alias.filter(
|
|
339
|
+
(entry): entry is StringAliasLike =>
|
|
340
|
+
isObject(entry) && typeof entry['find'] === 'string' && typeof entry['replacement'] === 'string',
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Message of a Vite/Rolldown build error, with its location and code frame when available. */
|
|
345
|
+
export function formatBuildError(error: unknown): string {
|
|
346
|
+
if (!(error instanceof Error)) return String(error)
|
|
347
|
+
const details = error as Error & {
|
|
348
|
+
plugin?: string
|
|
349
|
+
id?: string
|
|
350
|
+
loc?: { file?: string; line: number; column: number }
|
|
351
|
+
frame?: string
|
|
352
|
+
}
|
|
353
|
+
const lines = [details.plugin === undefined ? error.message : `[${details.plugin}] ${error.message}`]
|
|
354
|
+
const file = details.loc?.file ?? details.id
|
|
355
|
+
if (file !== undefined && !error.message.includes(file)) {
|
|
356
|
+
lines.push(details.loc === undefined ? ` --> ${file}` : ` --> ${file}:${details.loc.line}:${details.loc.column}`)
|
|
357
|
+
}
|
|
358
|
+
if (details.frame !== undefined && details.frame !== '' && !error.message.includes(details.frame)) {
|
|
359
|
+
lines.push(details.frame)
|
|
360
|
+
}
|
|
361
|
+
return lines.join('\n')
|
|
362
|
+
}
|