@c9up/ream 0.1.26 → 0.1.28
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 +65 -0
- package/dist/Ignitor.d.ts +115 -7
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +233 -37
- package/dist/Ignitor.js.map +1 -1
- package/dist/console/BaseCommand.d.ts +217 -0
- package/dist/console/BaseCommand.d.ts.map +1 -0
- package/dist/console/BaseCommand.js +340 -0
- package/dist/console/BaseCommand.js.map +1 -0
- package/dist/console/Console.d.ts +86 -0
- package/dist/console/Console.d.ts.map +1 -0
- package/dist/console/Console.js +134 -0
- package/dist/console/Console.js.map +1 -0
- package/dist/console/ExceptionHandler.d.ts +28 -0
- package/dist/console/ExceptionHandler.d.ts.map +1 -0
- package/dist/console/ExceptionHandler.js +76 -0
- package/dist/console/ExceptionHandler.js.map +1 -0
- package/dist/console/HelpCommand.d.ts +16 -0
- package/dist/console/HelpCommand.d.ts.map +1 -0
- package/dist/console/HelpCommand.js +40 -0
- package/dist/console/HelpCommand.js.map +1 -0
- package/dist/console/IndexGenerator.d.ts +24 -0
- package/dist/console/IndexGenerator.d.ts.map +1 -0
- package/dist/console/IndexGenerator.js +65 -0
- package/dist/console/IndexGenerator.js.map +1 -0
- package/dist/console/Kernel.d.ts +298 -0
- package/dist/console/Kernel.d.ts.map +1 -0
- package/dist/console/Kernel.js +1120 -0
- package/dist/console/Kernel.js.map +1 -0
- package/dist/console/ListCommand.d.ts +18 -0
- package/dist/console/ListCommand.d.ts.map +1 -0
- package/dist/console/ListCommand.js +60 -0
- package/dist/console/ListCommand.js.map +1 -0
- package/dist/console/assertions.d.ts +20 -0
- package/dist/console/assertions.d.ts.map +1 -0
- package/dist/console/assertions.js +84 -0
- package/dist/console/assertions.js.map +1 -0
- package/dist/console/cliui.d.ts +248 -0
- package/dist/console/cliui.d.ts.map +1 -0
- package/dist/console/cliui.js +587 -0
- package/dist/console/cliui.js.map +1 -0
- package/dist/console/decorators.d.ts +90 -0
- package/dist/console/decorators.d.ts.map +1 -0
- package/dist/console/decorators.js +142 -0
- package/dist/console/decorators.js.map +1 -0
- package/dist/console/index.d.ts +32 -0
- package/dist/console/index.d.ts.map +1 -0
- package/dist/console/index.js +24 -0
- package/dist/console/index.js.map +1 -0
- package/dist/console/loaders.d.ts +52 -0
- package/dist/console/loaders.d.ts.map +1 -0
- package/dist/console/loaders.js +131 -0
- package/dist/console/loaders.js.map +1 -0
- package/dist/console/parser.d.ts +86 -0
- package/dist/console/parser.d.ts.map +1 -0
- package/dist/console/parser.js +440 -0
- package/dist/console/parser.js.map +1 -0
- package/dist/console/prompts.d.ts +124 -0
- package/dist/console/prompts.d.ts.map +1 -0
- package/dist/console/prompts.js +364 -0
- package/dist/console/prompts.js.map +1 -0
- package/dist/console/tracing.d.ts +17 -0
- package/dist/console/tracing.d.ts.map +1 -0
- package/dist/console/tracing.js +11 -0
- package/dist/console/tracing.js.map +1 -0
- package/dist/console/types.d.ts +255 -0
- package/dist/console/types.d.ts.map +1 -0
- package/dist/console/types.js +25 -0
- package/dist/console/types.js.map +1 -0
- package/dist/console/ui.d.ts +20 -0
- package/dist/console/ui.d.ts.map +1 -0
- package/dist/console/ui.js +31 -0
- package/dist/console/ui.js.map +1 -0
- package/dist/console/utils.d.ts +27 -0
- package/dist/console/utils.d.ts.map +1 -0
- package/dist/console/utils.js +56 -0
- package/dist/console/utils.js.map +1 -0
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +40 -12
- package/dist/container/Container.js.map +1 -1
- package/dist/container/types.d.ts +14 -2
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +7 -0
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/decorators/Service.js +23 -3
- package/dist/decorators/Service.js.map +1 -1
- package/dist/errors/ReamError.d.ts +25 -0
- package/dist/errors/ReamError.d.ts.map +1 -1
- package/dist/errors/ReamError.js +5 -0
- package/dist/errors/ReamError.js.map +1 -1
- package/dist/errors/prettyPrintError.d.ts +9 -0
- package/dist/errors/prettyPrintError.d.ts.map +1 -0
- package/dist/errors/prettyPrintError.js +22 -0
- package/dist/errors/prettyPrintError.js.map +1 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts +29 -8
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +39 -1
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +13 -2
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/services/app.d.ts +1 -1
- package/dist/services/app.js +1 -1
- package/dist/services/console.d.ts +21 -0
- package/dist/services/console.d.ts.map +1 -0
- package/dist/services/console.js +35 -0
- package/dist/services/console.js.map +1 -0
- package/dist/testing/ApiResponse.d.ts +218 -0
- package/dist/testing/ApiResponse.d.ts.map +1 -0
- package/dist/testing/ApiResponse.js +576 -0
- package/dist/testing/ApiResponse.js.map +1 -0
- package/dist/testing/RequestBuilder.d.ts +132 -72
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +389 -222
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +34 -39
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +48 -56
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.js +3 -3
- package/dist/utils/Macroable.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +15 -2
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Ignitor.ts +340 -42
- package/src/console/BaseCommand.ts +467 -0
- package/src/console/Console.ts +174 -0
- package/src/console/ExceptionHandler.ts +91 -0
- package/src/console/HelpCommand.ts +37 -0
- package/src/console/IndexGenerator.ts +79 -0
- package/src/console/Kernel.ts +1382 -0
- package/src/console/ListCommand.ts +55 -0
- package/src/console/assertions.ts +114 -0
- package/src/console/cliui.ts +756 -0
- package/src/console/decorators.ts +199 -0
- package/src/console/index.ts +67 -0
- package/src/console/loaders.ts +145 -0
- package/src/console/parser.ts +598 -0
- package/src/console/prompts.ts +556 -0
- package/src/console/tracing.ts +22 -0
- package/src/console/types.ts +280 -0
- package/src/console/ui.ts +37 -0
- package/src/console/utils.ts +69 -0
- package/src/container/Container.ts +55 -13
- package/src/container/types.ts +15 -2
- package/src/decorators/Service.ts +28 -3
- package/src/errors/ReamError.ts +31 -0
- package/src/errors/prettyPrintError.ts +20 -0
- package/src/index.ts +30 -4
- package/src/middleware/Pipeline.ts +76 -5
- package/src/rpc/RpcRouter.ts +16 -2
- package/src/services/app.ts +1 -1
- package/src/services/console.ts +42 -0
- package/src/testing/ApiResponse.ts +691 -0
- package/src/testing/RequestBuilder.ts +498 -275
- package/src/testing/TestClient.ts +63 -86
- package/src/utils/Macroable.ts +3 -3
- package/dist/console/CommandRunner.d.ts +0 -18
- package/dist/console/CommandRunner.d.ts.map +0 -1
- package/dist/console/CommandRunner.js +0 -65
- package/dist/console/CommandRunner.js.map +0 -1
- package/src/console/CommandRunner.ts +0 -82
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@args` / `@flags` decorators — declarative command inputs (Console parity).
|
|
3
|
+
*
|
|
4
|
+
* Legacy (experimental) property decorators, matching the rest of the codebase
|
|
5
|
+
* and the project's swc config (`legacyDecorator: true`). Metadata is written
|
|
6
|
+
* onto the class as the static `args` / `flags` arrays the kernel reads, so a
|
|
7
|
+
* command satisfies the dispatch contract without inheriting anything.
|
|
8
|
+
*
|
|
9
|
+
* export default class Provision extends BaseCommand {
|
|
10
|
+
* static commandName = 'provision'
|
|
11
|
+
* static description = 'Create the owner account'
|
|
12
|
+
*
|
|
13
|
+
* @flags.string({ description: 'Owner email', required: true })
|
|
14
|
+
* declare email: string
|
|
15
|
+
*
|
|
16
|
+
* @flags.string({ default: 'Owner' })
|
|
17
|
+
* declare name: string
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Properties are declared with `declare` on purpose: under `target: ES2022`
|
|
21
|
+
* a real class field would be re-initialised to `undefined` at construction
|
|
22
|
+
* and wipe the value the kernel assigned.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { ReamError } from '../errors/ReamError.js'
|
|
26
|
+
import type { ArgumentMetaData, FlagMetaData } from './types.js'
|
|
27
|
+
|
|
28
|
+
/** `startServer` → `start-server`. */
|
|
29
|
+
function dashCase(value: string): string {
|
|
30
|
+
return value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Own-property lists per class.
|
|
35
|
+
*
|
|
36
|
+
* A subclass must not push into its parent's array — that would leak its flags
|
|
37
|
+
* onto every sibling. When the class does not own the list yet, seed a copy of
|
|
38
|
+
* whatever it inherited so extending a command keeps the parent's inputs.
|
|
39
|
+
*/
|
|
40
|
+
function ownList<T>(ctor: object, key: 'args' | 'flags'): T[] {
|
|
41
|
+
if (!Object.hasOwn(ctor, key)) {
|
|
42
|
+
const inherited: unknown = Reflect.get(ctor, key)
|
|
43
|
+
Reflect.set(ctor, key, Array.isArray(inherited) ? [...inherited] : [])
|
|
44
|
+
}
|
|
45
|
+
const list: unknown = Reflect.get(ctor, key)
|
|
46
|
+
return Array.isArray(list) ? list : []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ArgumentOptions {
|
|
50
|
+
/** Name shown in help. Defaults to the dash-cased property name. */
|
|
51
|
+
argumentName?: string
|
|
52
|
+
description?: string
|
|
53
|
+
/** Defaults to `true`, as in Console — positional arguments are expected. */
|
|
54
|
+
required?: boolean
|
|
55
|
+
default?: string | string[]
|
|
56
|
+
/** Accept an empty value instead of reporting it (Console `allowEmptyValue`). */
|
|
57
|
+
allowEmptyValue?: boolean
|
|
58
|
+
/** Transform or validate the value before assignment (Console `parse`). */
|
|
59
|
+
parse?: (value: string | string[]) => unknown
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FlagOptions {
|
|
63
|
+
/** Name used on the command line. Defaults to the dash-cased property name. */
|
|
64
|
+
flagName?: string
|
|
65
|
+
description?: string
|
|
66
|
+
/** Single-character shorthands, e.g. `['r']` for `-r`. */
|
|
67
|
+
alias?: string | string[]
|
|
68
|
+
default?: string | string[] | number | boolean
|
|
69
|
+
/** Defaults to `false` — a flag is optional unless stated otherwise. */
|
|
70
|
+
required?: boolean
|
|
71
|
+
/** Accept `--flag` with no value behind it (Console `allowEmptyValue`). */
|
|
72
|
+
allowEmptyValue?: boolean
|
|
73
|
+
/** Transform or validate the value before assignment (Console `parse`). */
|
|
74
|
+
parse?: (value: string | string[] | number | boolean) => unknown
|
|
75
|
+
/**
|
|
76
|
+
* Show `--no-<flag>` next to the flag in help. Booleans are always negatable;
|
|
77
|
+
* this only decides whether help says so.
|
|
78
|
+
*/
|
|
79
|
+
showNegatedVariantInHelp?: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reject an argument order the command line could never satisfy (Console).
|
|
84
|
+
*
|
|
85
|
+
* Exported because `BaseCommand.defineArgument()` declares arguments through a
|
|
86
|
+
* different path: one rule, or a decorated command would accept what the same
|
|
87
|
+
* declaration written by hand refuses.
|
|
88
|
+
*/
|
|
89
|
+
export function assertArgumentOrder(
|
|
90
|
+
declared: readonly ArgumentMetaData[],
|
|
91
|
+
incoming: Pick<ArgumentMetaData, 'argumentName' | 'required'>,
|
|
92
|
+
): void {
|
|
93
|
+
const last = declared[declared.length - 1]
|
|
94
|
+
if (last === undefined) return
|
|
95
|
+
|
|
96
|
+
if (last.type === 'spread') {
|
|
97
|
+
throw new ReamError(
|
|
98
|
+
'E_CONSOLE_INVALID_ARGUMENT',
|
|
99
|
+
`Argument "${incoming.argumentName}" comes after the spread argument "${last.argumentName}".`,
|
|
100
|
+
{ hint: 'A spread argument consumes the rest, so it must be declared last.' },
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
if (incoming.required && last.required === false) {
|
|
104
|
+
throw new ReamError(
|
|
105
|
+
'E_CONSOLE_INVALID_ARGUMENT',
|
|
106
|
+
`Required argument "${incoming.argumentName}" comes after the optional "${last.argumentName}".`,
|
|
107
|
+
{ hint: 'Declare the required arguments first, or make this one optional.' },
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build one argument's metadata from what was declared.
|
|
114
|
+
*
|
|
115
|
+
* The single builder for both declaration paths — the decorators and
|
|
116
|
+
* `BaseCommand.defineArgument()`. Two field-by-field copies is how a new option
|
|
117
|
+
* ends up honoured on one path and silently dropped on the other, which is
|
|
118
|
+
* exactly what happened to `allowEmptyValue`.
|
|
119
|
+
*/
|
|
120
|
+
export function buildArgument(
|
|
121
|
+
name: string,
|
|
122
|
+
options: Partial<ArgumentMetaData> = {},
|
|
123
|
+
): ArgumentMetaData {
|
|
124
|
+
return {
|
|
125
|
+
type: options.type ?? 'string',
|
|
126
|
+
propertyName: options.propertyName ?? name,
|
|
127
|
+
argumentName: options.argumentName ?? dashCase(name),
|
|
128
|
+
description: options.description,
|
|
129
|
+
// An argument with a default is optional by definition.
|
|
130
|
+
required: options.required ?? options.default === undefined,
|
|
131
|
+
default: options.default,
|
|
132
|
+
allowEmptyValue: options.allowEmptyValue,
|
|
133
|
+
parse: options.parse,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The same, for a flag. */
|
|
138
|
+
export function buildFlag(name: string, options: Partial<FlagMetaData> = {}): FlagMetaData {
|
|
139
|
+
return {
|
|
140
|
+
type: options.type ?? 'boolean',
|
|
141
|
+
propertyName: options.propertyName ?? name,
|
|
142
|
+
flagName: options.flagName ?? dashCase(name),
|
|
143
|
+
description: options.description,
|
|
144
|
+
alias: options.alias ?? [],
|
|
145
|
+
required: options.required ?? false,
|
|
146
|
+
default: options.default,
|
|
147
|
+
allowEmptyValue: options.allowEmptyValue,
|
|
148
|
+
parse: options.parse,
|
|
149
|
+
showNegatedVariantInHelp: options.showNegatedVariantInHelp,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function defineArgument(type: ArgumentMetaData['type'], options: ArgumentOptions) {
|
|
154
|
+
return (target: object, propertyKey: string | symbol): void => {
|
|
155
|
+
const argument = buildArgument(String(propertyKey), { ...options, type })
|
|
156
|
+
const list = ownList<ArgumentMetaData>(target.constructor, 'args')
|
|
157
|
+
assertArgumentOrder(list, argument)
|
|
158
|
+
list.push(argument)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function defineFlag(type: FlagMetaData['type'], options: FlagOptions) {
|
|
163
|
+
return (target: object, propertyKey: string | symbol): void => {
|
|
164
|
+
// `alias` is a string or a list at the decorator, always a list in the
|
|
165
|
+
// metadata — the only shape the builder cannot normalise for both paths.
|
|
166
|
+
const alias = options.alias === undefined ? [] : [options.alias].flat()
|
|
167
|
+
const list = ownList<FlagMetaData>(target.constructor, 'flags')
|
|
168
|
+
list.push(buildFlag(String(propertyKey), { ...options, type, alias }))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Positional arguments. Declaration order is the command-line order. */
|
|
173
|
+
export const args = {
|
|
174
|
+
/** A single positional value. */
|
|
175
|
+
string(options: ArgumentOptions = {}) {
|
|
176
|
+
return defineArgument('string', options)
|
|
177
|
+
},
|
|
178
|
+
/** Collects every remaining positional into an array. Declare it last. */
|
|
179
|
+
spread(options: ArgumentOptions = {}) {
|
|
180
|
+
return defineArgument('spread', options)
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Named flags. */
|
|
185
|
+
export const flags = {
|
|
186
|
+
string(options: FlagOptions = {}) {
|
|
187
|
+
return defineFlag('string', options)
|
|
188
|
+
},
|
|
189
|
+
boolean(options: FlagOptions = {}) {
|
|
190
|
+
return defineFlag('boolean', options)
|
|
191
|
+
},
|
|
192
|
+
number(options: FlagOptions = {}) {
|
|
193
|
+
return defineFlag('number', options)
|
|
194
|
+
},
|
|
195
|
+
/** Repeatable flag: `--tag a --tag b` → `['a', 'b']`. */
|
|
196
|
+
array(options: FlagOptions = {}) {
|
|
197
|
+
return defineFlag('array', options)
|
|
198
|
+
},
|
|
199
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@c9up/ream/console` — the console command surface.
|
|
3
|
+
*
|
|
4
|
+
* The SDK is ours — only the shape is borrowed: a command declares its name,
|
|
5
|
+
* its args and its flags, then runs.
|
|
6
|
+
*
|
|
7
|
+
* import { BaseCommand, args, flags } from '@c9up/ream/console'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { BaseCommand } from './BaseCommand.js'
|
|
11
|
+
export type { ConsoleOptions } from './Console.js'
|
|
12
|
+
export { Console } from './Console.js'
|
|
13
|
+
export type { Colors, StyleName, TaskOutcome } from './cliui.js'
|
|
14
|
+
export {
|
|
15
|
+
Action,
|
|
16
|
+
Box,
|
|
17
|
+
Logger,
|
|
18
|
+
Table,
|
|
19
|
+
TaskContext,
|
|
20
|
+
Tasks,
|
|
21
|
+
Ui,
|
|
22
|
+
} from './cliui.js'
|
|
23
|
+
export type { ArgumentOptions, FlagOptions } from './decorators.js'
|
|
24
|
+
export { args, flags } from './decorators.js'
|
|
25
|
+
export { ExceptionHandler } from './ExceptionHandler.js'
|
|
26
|
+
export { default as HelpCommand } from './HelpCommand.js'
|
|
27
|
+
export type { CommandsManifest } from './IndexGenerator.js'
|
|
28
|
+
export { IndexGenerator } from './IndexGenerator.js'
|
|
29
|
+
export type {
|
|
30
|
+
CommandExecutor,
|
|
31
|
+
CommandLoader,
|
|
32
|
+
ErrorRenderer,
|
|
33
|
+
GlobalFlagListener,
|
|
34
|
+
HandleResult,
|
|
35
|
+
KernelOptions,
|
|
36
|
+
KernelState,
|
|
37
|
+
} from './Kernel.js'
|
|
38
|
+
export { Kernel, serializeCommand } from './Kernel.js'
|
|
39
|
+
export { default as ListCommand } from './ListCommand.js'
|
|
40
|
+
export { FsLoader, ListLoader } from './loaders.js'
|
|
41
|
+
export type { ParseOptions } from './parser.js'
|
|
42
|
+
export { Parser, parseArgv } from './parser.js'
|
|
43
|
+
export type {
|
|
44
|
+
BooleanPromptOptions,
|
|
45
|
+
MultiplePromptOptions,
|
|
46
|
+
PromptChoice,
|
|
47
|
+
PromptOptions,
|
|
48
|
+
SelectPromptOptions,
|
|
49
|
+
} from './prompts.js'
|
|
50
|
+
export { Prompt, PromptTrap } from './prompts.js'
|
|
51
|
+
export { commandExec } from './tracing.js'
|
|
52
|
+
export type {
|
|
53
|
+
ArgumentMetaData,
|
|
54
|
+
CommandClass,
|
|
55
|
+
CommandInstance,
|
|
56
|
+
CommandOptions,
|
|
57
|
+
ExecutedCommand,
|
|
58
|
+
FlagMetaData,
|
|
59
|
+
ParsedInput,
|
|
60
|
+
} from './types.js'
|
|
61
|
+
export { isCommandClass } from './types.js'
|
|
62
|
+
export {
|
|
63
|
+
renderErrorWithSuggestions,
|
|
64
|
+
sortAlphabetically,
|
|
65
|
+
validateCommand,
|
|
66
|
+
validateCommandMetaData,
|
|
67
|
+
} from './utils.js'
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two loaders Console ships — `ListLoader` and `FsLoader`.
|
|
3
|
+
*
|
|
4
|
+
* A loader answers in two steps: the metadata of everything it offers, then the
|
|
5
|
+
* class for one of them. The kernel reads the first at boot and the second only
|
|
6
|
+
* when a command is asked for, so neither listing nor describing imports a
|
|
7
|
+
* module nobody is going to run.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdir } from 'node:fs/promises'
|
|
11
|
+
import { pathToFileURL } from 'node:url'
|
|
12
|
+
import type { CommandLoader } from './Kernel.js'
|
|
13
|
+
import { serializeCommand } from './Kernel.js'
|
|
14
|
+
import { type CommandClass, isCommandClass, type SerializedCommand } from './types.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Commands given as classes (Console `ListLoader`).
|
|
18
|
+
*
|
|
19
|
+
* The form a package uses to ship its commands: they are already imported, so
|
|
20
|
+
* there is nothing to defer.
|
|
21
|
+
*/
|
|
22
|
+
export class ListLoader implements CommandLoader {
|
|
23
|
+
readonly #commands: readonly CommandClass[]
|
|
24
|
+
|
|
25
|
+
constructor(commands: readonly CommandClass[]) {
|
|
26
|
+
this.#commands = [...commands]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getMetaData(): Promise<SerializedCommand[]> {
|
|
30
|
+
return this.#commands.map(serializeCommand)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getCommand(metadata: SerializedCommand): Promise<CommandClass | null> {
|
|
34
|
+
return this.#commands.find((command) => command.commandName === metadata.commandName) ?? null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Commands found in a directory (Console `FsLoader`).
|
|
40
|
+
*
|
|
41
|
+
* Reading the metadata imports each file — without a manifest there is no other
|
|
42
|
+
* way to know what a file declares, and Console's own `FsLoader` does the same. The
|
|
43
|
+
* gain is elsewhere: `getCommand()` never imports anything twice.
|
|
44
|
+
*/
|
|
45
|
+
export class FsLoader implements CommandLoader {
|
|
46
|
+
readonly #directory: URL
|
|
47
|
+
readonly #filter: ((filePath: string) => boolean) | undefined
|
|
48
|
+
readonly #commands = new Map<string, CommandClass>()
|
|
49
|
+
readonly #paths = new Map<string, string>()
|
|
50
|
+
#onSkipped: ((fileName: string) => void) | undefined
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param directory Where to look, as a path or a URL.
|
|
54
|
+
* @param filter Files to keep. Everything is loaded when it is omitted.
|
|
55
|
+
*/
|
|
56
|
+
constructor(directory: string | URL, filter?: (filePath: string) => boolean) {
|
|
57
|
+
this.#directory = typeof directory === 'string' ? pathToFileURL(`${directory}/`) : directory
|
|
58
|
+
this.#filter = filter
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Called for a file that exports no command.
|
|
63
|
+
*
|
|
64
|
+
* Reported rather than thrown: the directory is scanned by convention, so a
|
|
65
|
+
* stray helper there must not make the whole CLI unusable.
|
|
66
|
+
*/
|
|
67
|
+
onSkipped(handler: (fileName: string) => void): this {
|
|
68
|
+
this.#onSkipped = handler
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getMetaData(): Promise<SerializedCommand[]> {
|
|
73
|
+
let entries: Array<{ name: string; isDirectory(): boolean; parentPath?: string }>
|
|
74
|
+
try {
|
|
75
|
+
entries = await readdir(this.#directory, { withFileTypes: true, recursive: true })
|
|
76
|
+
} catch {
|
|
77
|
+
// No such directory — perfectly normal.
|
|
78
|
+
return []
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const files = entries
|
|
82
|
+
.filter((entry) => !entry.isDirectory() && isCommandFile(entry.name))
|
|
83
|
+
.map((entry) => new URL(entry.name, directoryUrl(entry.parentPath, this.#directory)))
|
|
84
|
+
.filter((file) => this.#filter === undefined || this.#filter(file.pathname))
|
|
85
|
+
.sort((a, b) => a.href.localeCompare(b.href))
|
|
86
|
+
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
const command = commandOf(await this.import(file))
|
|
89
|
+
if (command === undefined) {
|
|
90
|
+
this.#onSkipped?.(file.pathname.split('/').pop() ?? file.pathname)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
this.#commands.set(command.commandName, command)
|
|
94
|
+
this.#paths.set(command.commandName, relativeTo(this.#directory, file))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return [...this.#commands.values()].map((command) => ({
|
|
98
|
+
...serializeCommand(command),
|
|
99
|
+
filePath: this.#paths.get(command.commandName),
|
|
100
|
+
}))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getCommand(metadata: SerializedCommand): Promise<CommandClass | null> {
|
|
104
|
+
return this.#commands.get(metadata.commandName) ?? null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* How a file is imported. Overridable because an application may compile its
|
|
109
|
+
* own sources — Ream's Ignitor routes this through the app's importer.
|
|
110
|
+
*/
|
|
111
|
+
protected import(file: URL): Promise<unknown> {
|
|
112
|
+
return import(file.href)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `./make/controller.ts` — how a manifest refers back to the file. */
|
|
117
|
+
function relativeTo(directory: URL, file: URL): string {
|
|
118
|
+
return `./${file.href.slice(directory.href.length)}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `.ts` / `.js` modules only, skipping declarations and `_private` helpers. */
|
|
122
|
+
function isCommandFile(name: string): boolean {
|
|
123
|
+
if (name.startsWith('_') || name.endsWith('.d.ts')) return false
|
|
124
|
+
return /\.(ts|js|mts|mjs)$/.test(name)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* `readdir(recursive)` reports nested entries via `parentPath`; older Node
|
|
129
|
+
* builds omit it for top-level entries, where the base directory is correct.
|
|
130
|
+
*/
|
|
131
|
+
function directoryUrl(parentPath: string | undefined, fallback: URL): URL {
|
|
132
|
+
if (parentPath === undefined) return fallback
|
|
133
|
+
// `readdir` echoes the path it was given, which already ends with a slash
|
|
134
|
+
// when it came from a URL. Adding a second one yields `…/commands//greet.ts`:
|
|
135
|
+
// still importable, but wrong the moment the path is written to a manifest.
|
|
136
|
+
const href = pathToFileURL(parentPath).href
|
|
137
|
+
return new URL(href.endsWith('/') ? href : `${href}/`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The command a module default-exports, or undefined when it exports none. */
|
|
141
|
+
function commandOf(mod: unknown): CommandClass | undefined {
|
|
142
|
+
if (typeof mod !== 'object' || mod === null) return undefined
|
|
143
|
+
const value = Reflect.get(mod, 'default')
|
|
144
|
+
return isCommandClass(value) ? value : undefined
|
|
145
|
+
}
|