@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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +482 -2
  3. package/bin/znight.mjs +17 -0
  4. package/dist/cli.d.mts +1 -0
  5. package/dist/cli.mjs +13 -0
  6. package/dist/cli.mjs.map +1 -0
  7. package/dist/dev/orphan-guard.d.mts +1 -0
  8. package/dist/dev/orphan-guard.mjs +47 -0
  9. package/dist/dev/orphan-guard.mjs.map +1 -0
  10. package/dist/index.d.mts +679 -0
  11. package/dist/index.mjs +1 -0
  12. package/dist/main-BWsiUsxN.mjs +3865 -0
  13. package/dist/main-BWsiUsxN.mjs.map +1 -0
  14. package/dist/orphan-Dn-2xMBd.mjs +45 -0
  15. package/dist/orphan-Dn-2xMBd.mjs.map +1 -0
  16. package/package.json +71 -4
  17. package/src/cli.ts +11 -0
  18. package/src/command.ts +217 -0
  19. package/src/commands/build.ts +159 -0
  20. package/src/commands/dev.ts +249 -0
  21. package/src/commands/generate.ts +304 -0
  22. package/src/commands/new.ts +245 -0
  23. package/src/commands/openapi.ts +160 -0
  24. package/src/commands/schematic.ts +110 -0
  25. package/src/commands/shared.ts +106 -0
  26. package/src/commands/start.ts +79 -0
  27. package/src/context.ts +126 -0
  28. package/src/dev/orphan-guard.ts +49 -0
  29. package/src/dev/orphan.ts +74 -0
  30. package/src/dev/supervisor.ts +175 -0
  31. package/src/errors.ts +64 -0
  32. package/src/index.ts +73 -0
  33. package/src/integrations/compiler.ts +360 -0
  34. package/src/integrations/openapi.ts +97 -0
  35. package/src/integrations/process.ts +180 -0
  36. package/src/integrations/prompts.ts +108 -0
  37. package/src/integrations/resolve.ts +38 -0
  38. package/src/integrations/vite.ts +362 -0
  39. package/src/integrations/watch.ts +243 -0
  40. package/src/main.ts +195 -0
  41. package/src/names.ts +106 -0
  42. package/src/output.ts +110 -0
  43. package/src/runtime.ts +86 -0
  44. package/src/scaffold/create.ts +65 -0
  45. package/src/scaffold/options.ts +103 -0
  46. package/src/scaffold/templates.ts +771 -0
  47. package/src/scaffold/versions.ts +25 -0
  48. package/src/schematics/index.ts +422 -0
  49. 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
+ }