@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
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript runtimes the application can run on (`znight dev`, `znight start`) and executable
|
|
3
|
+
* lookup in `PATH`.
|
|
4
|
+
*/
|
|
5
|
+
import { accessSync, constants, statSync } from 'node:fs'
|
|
6
|
+
import { delimiter, isAbsolute, join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
/** Runtime of the application process. */
|
|
9
|
+
export type RuntimeName = 'node' | 'bun'
|
|
10
|
+
|
|
11
|
+
/** `--runtime` values: `auto` picks Bun when the CLI itself runs on Bun, Node.js otherwise. */
|
|
12
|
+
export type RuntimeOption = RuntimeName | 'auto'
|
|
13
|
+
|
|
14
|
+
/** The runtime executing the CLI. */
|
|
15
|
+
export interface HostRuntime {
|
|
16
|
+
readonly name: RuntimeName
|
|
17
|
+
/** Absolute path of the running executable (`process.execPath`). */
|
|
18
|
+
readonly execPath: string
|
|
19
|
+
readonly platform: NodeJS.Platform
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Detects the runtime executing this process. */
|
|
23
|
+
export function detectHostRuntime(): HostRuntime {
|
|
24
|
+
const isBun = typeof process.versions['bun'] === 'string'
|
|
25
|
+
return { name: isBun ? 'bun' : 'node', execPath: process.execPath, platform: process.platform }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Resolves `auto`. */
|
|
29
|
+
export function resolveRuntime(option: RuntimeOption | undefined, host: HostRuntime): RuntimeName {
|
|
30
|
+
return option === undefined || option === 'auto' ? host.name : option
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Finds an executable in `PATH` (`PATHEXT` extensions on Windows). Absolute paths are checked as is.
|
|
35
|
+
* Returns `undefined` when not found.
|
|
36
|
+
*/
|
|
37
|
+
export function which(
|
|
38
|
+
command: string,
|
|
39
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
40
|
+
platform: NodeJS.Platform = process.platform,
|
|
41
|
+
): string | undefined {
|
|
42
|
+
const extensions =
|
|
43
|
+
platform === 'win32' ? ['', ...(env['PATHEXT'] ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)] : ['']
|
|
44
|
+
const candidates = isAbsolute(command)
|
|
45
|
+
? [command]
|
|
46
|
+
: (env['PATH'] ?? env['Path'] ?? '')
|
|
47
|
+
.split(platform === 'win32' ? ';' : delimiter)
|
|
48
|
+
.filter(directory => directory !== '')
|
|
49
|
+
.map(directory => join(directory, command))
|
|
50
|
+
for (const candidate of candidates) {
|
|
51
|
+
for (const extension of extensions) {
|
|
52
|
+
const file = candidate + extension
|
|
53
|
+
if (isExecutable(file, platform)) return file
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isExecutable(file: string, platform: NodeJS.Platform): boolean {
|
|
60
|
+
try {
|
|
61
|
+
if (!statSync(file).isFile()) return false
|
|
62
|
+
if (platform !== 'win32') accessSync(file, constants.X_OK)
|
|
63
|
+
return true
|
|
64
|
+
} catch {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The executable of `runtime`: the running executable when it is that runtime, otherwise the one
|
|
71
|
+
* found in `PATH`.
|
|
72
|
+
*/
|
|
73
|
+
export function runtimeExecutable(
|
|
74
|
+
runtime: RuntimeName,
|
|
75
|
+
host: HostRuntime,
|
|
76
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
77
|
+
): string | undefined {
|
|
78
|
+
if (runtime === host.name) return host.execPath
|
|
79
|
+
return which(runtime, env, host.platform)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Runtime arguments placed before the entry file. */
|
|
83
|
+
export function runtimeArguments(runtime: RuntimeName, extra: readonly string[] = []): string[] {
|
|
84
|
+
// Vite emits source maps for server builds: map stack traces to the TypeScript sources.
|
|
85
|
+
return runtime === 'node' ? ['--enable-source-maps', ...extra] : [...extra]
|
|
86
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Writing a new project to disk, initialising git and installing dependencies. */
|
|
2
|
+
import type { ProcessService, RunResult } from '../integrations/process.ts'
|
|
3
|
+
import type { PackageManager } from './options.ts'
|
|
4
|
+
import type { ProjectFile } from './templates.ts'
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { dirname, join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
import { installCommand } from './options.ts'
|
|
10
|
+
|
|
11
|
+
/** Entries tolerated in the target directory (a fresh clone, editor settings). */
|
|
12
|
+
const IGNORABLE_ENTRIES = new Set(['.git', '.DS_Store', 'Thumbs.db', '.idea', '.vscode'])
|
|
13
|
+
|
|
14
|
+
/** State of the target directory of `znight new`. */
|
|
15
|
+
export type TargetState = 'missing' | 'empty' | 'not-empty' | 'file'
|
|
16
|
+
|
|
17
|
+
/** Inspects the target directory. */
|
|
18
|
+
export function targetState(directory: string): TargetState {
|
|
19
|
+
let stats: ReturnType<typeof statSync>
|
|
20
|
+
try {
|
|
21
|
+
stats = statSync(directory)
|
|
22
|
+
} catch {
|
|
23
|
+
return 'missing'
|
|
24
|
+
}
|
|
25
|
+
if (!stats.isDirectory()) return 'file'
|
|
26
|
+
return readdirSync(directory).every(entry => IGNORABLE_ENTRIES.has(entry)) ? 'empty' : 'not-empty'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Writes `files` under `directory` (created when missing). Returns the written paths. */
|
|
30
|
+
export function writeProject(directory: string, files: readonly ProjectFile[]): string[] {
|
|
31
|
+
mkdirSync(directory, { recursive: true })
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const target = join(directory, file.path)
|
|
34
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
35
|
+
writeFileSync(target, file.content)
|
|
36
|
+
}
|
|
37
|
+
return files.map(file => file.path)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Outcome of {@link initGit}. */
|
|
41
|
+
export type GitOutcome = 'initialized' | 'inside-repository' | 'unavailable' | 'failed'
|
|
42
|
+
|
|
43
|
+
/** `git init` in `directory`, unless it already is inside a git work tree (monorepos). */
|
|
44
|
+
export async function initGit(
|
|
45
|
+
processes: ProcessService,
|
|
46
|
+
directory: string,
|
|
47
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
48
|
+
): Promise<GitOutcome> {
|
|
49
|
+
const inside = await processes.run('git', ['rev-parse', '--is-inside-work-tree'], { cwd: directory, env })
|
|
50
|
+
if (inside.error !== undefined) return 'unavailable'
|
|
51
|
+
if (inside.code === 0 && inside.stdout.trim() === 'true') return 'inside-repository'
|
|
52
|
+
const init = await processes.run('git', ['init', '--quiet'], { cwd: directory, env })
|
|
53
|
+
return init.code === 0 ? 'initialized' : 'failed'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `<pm> install` in `directory`, output captured. */
|
|
57
|
+
export function installDependencies(
|
|
58
|
+
processes: ProcessService,
|
|
59
|
+
directory: string,
|
|
60
|
+
packageManager: PackageManager,
|
|
61
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
62
|
+
): Promise<RunResult> {
|
|
63
|
+
const [command, ...args] = installCommand(packageManager)
|
|
64
|
+
return processes.run(command, args, { cwd: directory, env })
|
|
65
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Choices of `znight new`. */
|
|
2
|
+
|
|
3
|
+
/** Package managers supported by `znight new`. */
|
|
4
|
+
export type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
|
|
5
|
+
|
|
6
|
+
/** HTTP adapters of `@znight/http`. */
|
|
7
|
+
export type HttpAdapterName = 'node' | 'fastify' | 'express' | 'hono'
|
|
8
|
+
|
|
9
|
+
/** Optional features of a new project. */
|
|
10
|
+
export type Feature = 'config' | 'observability' | 'messaging' | 'schedule' | 'openapi'
|
|
11
|
+
|
|
12
|
+
/** Every package manager, in display order. */
|
|
13
|
+
export const PACKAGE_MANAGERS: readonly PackageManager[] = ['bun', 'npm', 'pnpm', 'yarn']
|
|
14
|
+
|
|
15
|
+
/** Every HTTP adapter, in display order. */
|
|
16
|
+
export const HTTP_ADAPTERS: readonly HttpAdapterName[] = ['node', 'fastify', 'express', 'hono']
|
|
17
|
+
|
|
18
|
+
/** Every feature, in display (and canonical) order. */
|
|
19
|
+
export const FEATURES: readonly Feature[] = ['config', 'observability', 'messaging', 'schedule', 'openapi']
|
|
20
|
+
|
|
21
|
+
/** Features selected when none are given. */
|
|
22
|
+
export const DEFAULT_FEATURES: readonly Feature[] = ['config', 'observability']
|
|
23
|
+
|
|
24
|
+
/** Labels shown by the prompts and the help. */
|
|
25
|
+
export const ADAPTER_LABELS: Readonly<Record<HttpAdapterName, string>> = {
|
|
26
|
+
node: 'node:http (built-in, zero dependency)',
|
|
27
|
+
fastify: 'Fastify',
|
|
28
|
+
express: 'Express',
|
|
29
|
+
hono: 'Hono',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Labels shown by the prompts. */
|
|
33
|
+
export const FEATURE_LABELS: Readonly<Record<Feature, string>> = {
|
|
34
|
+
config: 'Config: type-safe configuration classes (@znight/config)',
|
|
35
|
+
observability: 'Observability: structured logs, metrics, tracing (@znight/observability)',
|
|
36
|
+
messaging: 'Messaging: message consumers and publishing (@znight/messaging)',
|
|
37
|
+
schedule: 'Schedule: cron, interval and timeout jobs (@znight/schedule)',
|
|
38
|
+
openapi: 'OpenAPI: openapi.json and an API reference at /docs (@znight/openapi)',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Everything that determines the files of a new project. */
|
|
42
|
+
export interface ProjectOptions {
|
|
43
|
+
/** npm package name. */
|
|
44
|
+
readonly name: string
|
|
45
|
+
readonly packageManager: PackageManager
|
|
46
|
+
readonly adapter: HttpAdapterName
|
|
47
|
+
/** Selected features, in canonical order. */
|
|
48
|
+
readonly features: readonly Feature[]
|
|
49
|
+
/** Version range base of the `@znight/*` packages (the CLI version). */
|
|
50
|
+
readonly znightVersion: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parses `--features` (`config,observability`, `none`, `all`). Returns the features in canonical
|
|
55
|
+
* order, or the unknown names.
|
|
56
|
+
*/
|
|
57
|
+
export function parseFeatures(value: string): { features: Feature[] } | { unknown: string[] } {
|
|
58
|
+
const names = value
|
|
59
|
+
.split(',')
|
|
60
|
+
.map(name => name.trim().toLowerCase())
|
|
61
|
+
.filter(name => name !== '')
|
|
62
|
+
if (names.length === 1 && names[0] === 'none') return { features: [] }
|
|
63
|
+
if (names.length === 1 && names[0] === 'all') return { features: [...FEATURES] }
|
|
64
|
+
const unknown = names.filter(name => !FEATURES.includes(name as Feature))
|
|
65
|
+
if (unknown.length > 0) return { unknown }
|
|
66
|
+
return { features: FEATURES.filter(feature => names.includes(feature)) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Default package manager: the one that launched the CLI (`npm_config_user_agent`: `pnpm dlx`,
|
|
71
|
+
* `yarn dlx`, `bunx`, `npx`), else Bun when running on Bun, else npm.
|
|
72
|
+
*/
|
|
73
|
+
export function detectPackageManager(
|
|
74
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
75
|
+
runningOnBun: boolean,
|
|
76
|
+
): PackageManager {
|
|
77
|
+
const agent = env['npm_config_user_agent'] ?? ''
|
|
78
|
+
const name = agent.split('/')[0]
|
|
79
|
+
if (name === 'bun' || name === 'npm' || name === 'pnpm' || name === 'yarn') return name
|
|
80
|
+
return runningOnBun ? 'bun' : 'npm'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `bun install`, `npm install`... */
|
|
84
|
+
export function installCommand(pm: PackageManager): readonly [string, ...string[]] {
|
|
85
|
+
return [pm, 'install']
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `bun run dev`, `npm run dev`, `pnpm dev`, `yarn dev`. */
|
|
89
|
+
export function runScript(pm: PackageManager, script: string): string {
|
|
90
|
+
return pm === 'pnpm' || pm === 'yarn' ? `${pm} ${script}` : `${pm} run ${script}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Runs a binary of the project: `npx znight`, `bunx znight`, `pnpm znight`, `yarn znight`. */
|
|
94
|
+
export function execBinary(pm: PackageManager, binary: string): string {
|
|
95
|
+
switch (pm) {
|
|
96
|
+
case 'npm':
|
|
97
|
+
return `npx ${binary}`
|
|
98
|
+
case 'bun':
|
|
99
|
+
return `bunx ${binary}`
|
|
100
|
+
default:
|
|
101
|
+
return `${pm} ${binary}`
|
|
102
|
+
}
|
|
103
|
+
}
|