@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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console command contract — the STATIC shape the kernel dispatches against.
|
|
3
|
+
*
|
|
4
|
+
* Console parity: a command is a class carrying its own metadata as statics
|
|
5
|
+
* (`commandName`, `description`, `options`) plus the argument/flag metadata
|
|
6
|
+
* collected by the `@args` / `@flags` decorators. The kernel never requires the
|
|
7
|
+
* class to extend {@link BaseCommand}: ANY class exposing these statics is
|
|
8
|
+
* dispatchable.
|
|
9
|
+
*
|
|
10
|
+
* That is deliberate. Framework-agnostic packages (atlas and friends) must not
|
|
11
|
+
* import `@c9up/ream` — the same constraint that made the old `AtlasCommand`
|
|
12
|
+
* duck-type the `{ name, description, run }` object shape. Keeping the contract
|
|
13
|
+
* on statics lets those packages declare classes with typed args/flags and stay
|
|
14
|
+
* dependency-free.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Per-command behaviour switches. Mirrors Console's `CommandOptions`. */
|
|
18
|
+
export interface CommandOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Boot the application (providers, DB, container) before `run()`.
|
|
21
|
+
*
|
|
22
|
+
* Defaults to `false`, as in Console: a scaffolding command has no business
|
|
23
|
+
* opening a database connection. Commands that resolve services from the
|
|
24
|
+
* container must opt in.
|
|
25
|
+
*/
|
|
26
|
+
startApp?: boolean
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keep the process alive after `run()` resolves (long-running commands such
|
|
30
|
+
* as a queue worker). Defaults to `false` — the kernel tears the app down and
|
|
31
|
+
* lets the process exit.
|
|
32
|
+
*/
|
|
33
|
+
staysAlive?: boolean
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Accept flags the command did not declare instead of failing. Off by
|
|
37
|
+
* default so a typo (`--warm` for `--warn`) is reported, not swallowed.
|
|
38
|
+
*/
|
|
39
|
+
allowUnknownFlags?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Metadata recorded by an `@args.*` decorator. */
|
|
43
|
+
export interface ArgumentMetaData {
|
|
44
|
+
type: 'string' | 'spread'
|
|
45
|
+
/** Class property the parsed value is assigned to. */
|
|
46
|
+
propertyName: string
|
|
47
|
+
/** Name shown in help output (defaults to the dash-cased property name). */
|
|
48
|
+
argumentName: string
|
|
49
|
+
description?: string
|
|
50
|
+
required: boolean
|
|
51
|
+
default?: string | string[]
|
|
52
|
+
/**
|
|
53
|
+
* Accept an empty value (`ream greet ""`). Rejected by default, as in Console: an
|
|
54
|
+
* empty positional is nearly always a shell variable that did not expand.
|
|
55
|
+
*/
|
|
56
|
+
allowEmptyValue?: boolean
|
|
57
|
+
/** Transforms/validates the raw value before it is assigned (Console `parse`). */
|
|
58
|
+
parse?: (value: string | string[]) => unknown
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Metadata recorded by a `@flags.*` decorator. */
|
|
62
|
+
export interface FlagMetaData {
|
|
63
|
+
type: 'string' | 'boolean' | 'number' | 'array'
|
|
64
|
+
/** Class property the parsed value is assigned to. */
|
|
65
|
+
propertyName: string
|
|
66
|
+
/** Name used on the command line (defaults to the dash-cased property name). */
|
|
67
|
+
flagName: string
|
|
68
|
+
description?: string
|
|
69
|
+
/** Single-character shorthands, e.g. `['r']` for `-r`. */
|
|
70
|
+
alias: string[]
|
|
71
|
+
default?: string | string[] | number | boolean
|
|
72
|
+
required: boolean
|
|
73
|
+
/**
|
|
74
|
+
* Accept `--name` with nothing behind it, as an empty value rather than an
|
|
75
|
+
* error (Console `allowEmptyValue`).
|
|
76
|
+
*/
|
|
77
|
+
allowEmptyValue?: boolean
|
|
78
|
+
/** Transforms/validates the coerced value before it is assigned (Console `parse`). */
|
|
79
|
+
parse?: (value: string | string[] | number | boolean) => unknown
|
|
80
|
+
/** List `--no-<flag>` alongside the flag in help (booleans only). */
|
|
81
|
+
showNegatedVariantInHelp?: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** What the kernel produces from argv and assigns onto the command instance. */
|
|
85
|
+
/**
|
|
86
|
+
* The parsed CLI input, in the shape Console publishes it.
|
|
87
|
+
*
|
|
88
|
+
* Positionals are a LIST, in declaration order, and flags are keyed by their
|
|
89
|
+
* COMMAND-LINE name — `parsed.flags['user-email']`, not `parsed.flags.userEmail`.
|
|
90
|
+
* The property names are what the values are assigned TO on the command; the
|
|
91
|
+
* parsed bag describes what was typed.
|
|
92
|
+
*/
|
|
93
|
+
export interface ParsedInput {
|
|
94
|
+
args: unknown[]
|
|
95
|
+
flags: Record<string, unknown>
|
|
96
|
+
/**
|
|
97
|
+
* Names of the flags that were passed but not declared (Console
|
|
98
|
+
* `this.parsed.unknownFlags`). Their values are in {@link flags}; this is the
|
|
99
|
+
* list a command inspects to know what it was handed but does not know about.
|
|
100
|
+
*/
|
|
101
|
+
unknownFlags: string[]
|
|
102
|
+
/**
|
|
103
|
+
* Positionals beyond what the command declared.
|
|
104
|
+
*
|
|
105
|
+
* Normally none: an undeclared positional is an error. A command running with
|
|
106
|
+
* `allowUnknownFlags` is a proxy, so they are kept here instead of being
|
|
107
|
+
* dropped — forwarding them is the whole point of such a command.
|
|
108
|
+
*/
|
|
109
|
+
extraArgs: string[]
|
|
110
|
+
/** Console's name for {@link extraArgs} — the same array, not a copy. */
|
|
111
|
+
_: string[]
|
|
112
|
+
/**
|
|
113
|
+
* The arguments node itself was started with (Console `nodeArgs`), filled only
|
|
114
|
+
* for the command invoked from the command line.
|
|
115
|
+
*/
|
|
116
|
+
nodeArgs: string[]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The instance side of a command.
|
|
121
|
+
*
|
|
122
|
+
* Only `run()` is required. The lifecycle hooks are optional: the kernel calls
|
|
123
|
+
* whichever the command defines, in Console's order — prepare, interact, run,
|
|
124
|
+
* completed.
|
|
125
|
+
*/
|
|
126
|
+
export interface CommandInstance {
|
|
127
|
+
/**
|
|
128
|
+
* Whatever it returns becomes `result` on the executed command.
|
|
129
|
+
*
|
|
130
|
+
* Parameters are allowed: the container injects them (Console `@inject()` on a
|
|
131
|
+
* lifecycle method), and a no-arg signature would reject those commands.
|
|
132
|
+
*/
|
|
133
|
+
run(...args: never[]): unknown
|
|
134
|
+
prepare?(...args: never[]): void | Promise<void>
|
|
135
|
+
interact?(...args: never[]): void | Promise<void>
|
|
136
|
+
/** Returning `true` marks the error handled, stopping its propagation. */
|
|
137
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: Console contract — returning `true` marks the error handled, returning nothing is the normal case
|
|
138
|
+
completed?(...args: never[]): boolean | void | Promise<boolean | void>
|
|
139
|
+
/** Set by the command to force a code; the kernel fills it in otherwise. */
|
|
140
|
+
exitCode?: number
|
|
141
|
+
/** Filled by the kernel before `completed()` runs. */
|
|
142
|
+
error?: unknown
|
|
143
|
+
/** Filled by the kernel with what `run()` returned. */
|
|
144
|
+
result?: unknown
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A command's metadata as plain data (Console `serialize`).
|
|
149
|
+
*
|
|
150
|
+
* JSON-safe on purpose: the `parse` callbacks carried by the live metadata are
|
|
151
|
+
* dropped, so this can be printed, cached or sent over a wire without a
|
|
152
|
+
* function silently disappearing at the boundary.
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* How one positional argument is parsed (Console `ArgumentsParserOptions`).
|
|
156
|
+
*
|
|
157
|
+
* Derived from the declarations by `BaseCommand.getParserOptions()`. Ream's own
|
|
158
|
+
* parser reads the declarations directly; this shape exists so code ported from
|
|
159
|
+
* Console — which inspects it — keeps working.
|
|
160
|
+
*/
|
|
161
|
+
export interface ArgumentsParserOptions {
|
|
162
|
+
type: 'string' | 'spread'
|
|
163
|
+
default?: string | string[]
|
|
164
|
+
parse?: (value: string | string[]) => unknown
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** How the flags are parsed, grouped by type (Console `FlagsParserOptions`). */
|
|
168
|
+
export interface FlagsParserOptions {
|
|
169
|
+
all: string[]
|
|
170
|
+
string: string[]
|
|
171
|
+
boolean: string[]
|
|
172
|
+
number: string[]
|
|
173
|
+
array: string[]
|
|
174
|
+
/** Flag name → its single-character shorthands. */
|
|
175
|
+
alias: Record<string, string[]>
|
|
176
|
+
count: string[]
|
|
177
|
+
/** Flag name → its `parse` callback. */
|
|
178
|
+
coerce: Record<string, (value: never) => unknown>
|
|
179
|
+
default: Record<string, unknown>
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface SerializedCommand {
|
|
183
|
+
commandName: string
|
|
184
|
+
/**
|
|
185
|
+
* Where the command was found, relative to the directory it was loaded from.
|
|
186
|
+
*
|
|
187
|
+
* Set by loaders that read files, and by nothing else: it is what lets a
|
|
188
|
+
* generated manifest import a command without scanning anything.
|
|
189
|
+
*/
|
|
190
|
+
filePath?: string
|
|
191
|
+
/** The part before `:` — `make` for `make:controller`, null when there is none. */
|
|
192
|
+
namespace: string | null
|
|
193
|
+
description: string
|
|
194
|
+
help?: string | string[]
|
|
195
|
+
aliases: string[]
|
|
196
|
+
options: CommandOptions
|
|
197
|
+
args: Array<Omit<ArgumentMetaData, 'parse'>>
|
|
198
|
+
flags: Array<Omit<FlagMetaData, 'parse'>>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A snapshot of a command that has run (Console `toJSON`).
|
|
203
|
+
*
|
|
204
|
+
* Unlike {@link SerializedCommand} this is about one execution: the values it
|
|
205
|
+
* received and what it produced. `args` / `flags` are the parsed VALUES, not
|
|
206
|
+
* their declarations.
|
|
207
|
+
*/
|
|
208
|
+
export interface CommandSnapshot {
|
|
209
|
+
commandName: string
|
|
210
|
+
options: CommandOptions
|
|
211
|
+
/** Positional VALUES in declaration order — Console exposes a list here. */
|
|
212
|
+
args: unknown[]
|
|
213
|
+
flags: Record<string, unknown>
|
|
214
|
+
error: unknown
|
|
215
|
+
result: unknown
|
|
216
|
+
exitCode: number | undefined
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A command that has run.
|
|
221
|
+
*
|
|
222
|
+
* The three outcome fields are guaranteed here, which is what `exec()` returns:
|
|
223
|
+
* without it the documented `command.exitCode` / `.result` / `.error` does not
|
|
224
|
+
* typecheck for a caller, and the only way to read them is a reflective escape
|
|
225
|
+
* hatch — which is exactly what the tests had ended up doing.
|
|
226
|
+
*/
|
|
227
|
+
/**
|
|
228
|
+
* Assertions available on a command that has run.
|
|
229
|
+
*
|
|
230
|
+
* Part of what `exec()` returns, so a caller can use them without the type
|
|
231
|
+
* hiding what exists at runtime. The kernel attaches them to every command it
|
|
232
|
+
* runs, including one that does not extend `BaseCommand`.
|
|
233
|
+
*/
|
|
234
|
+
export interface CommandAssertions {
|
|
235
|
+
assertSucceeded(): void
|
|
236
|
+
assertFailed(): void
|
|
237
|
+
assertExitCode(expected: number): void
|
|
238
|
+
assertNotExitCode(unexpected: number): void
|
|
239
|
+
assertLog(message: string, stream?: 'stdout' | 'stderr'): void
|
|
240
|
+
assertLogMatches(pattern: RegExp, stream?: 'stdout' | 'stderr'): void
|
|
241
|
+
assertTableRows(expected: readonly (readonly string[])[]): void
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface ExecutedCommand extends CommandInstance, CommandAssertions {
|
|
245
|
+
exitCode: number
|
|
246
|
+
error: unknown
|
|
247
|
+
result: unknown
|
|
248
|
+
/** A snapshot of this execution (Console `toJSON`). */
|
|
249
|
+
toJSON(): CommandSnapshot
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The static side. `args` / `flags` are optional so a command with neither
|
|
254
|
+
* still satisfies the contract without declaring empty arrays.
|
|
255
|
+
*/
|
|
256
|
+
export interface CommandClass {
|
|
257
|
+
// Not `new ()`: a command built through the container receives its injected
|
|
258
|
+
// dependencies, and a zero-arg signature rejects every such class.
|
|
259
|
+
new (...args: never[]): CommandInstance
|
|
260
|
+
commandName: string
|
|
261
|
+
description: string
|
|
262
|
+
/** Alternative names the command also answers to (Console `static aliases`). */
|
|
263
|
+
aliases?: readonly string[]
|
|
264
|
+
options?: CommandOptions
|
|
265
|
+
args?: readonly ArgumentMetaData[]
|
|
266
|
+
flags?: readonly FlagMetaData[]
|
|
267
|
+
/** Longer help text printed under the usage line by `<cmd> --help`. */
|
|
268
|
+
help?: string | string[]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Narrowing guard used by the kernel when loading modules. */
|
|
272
|
+
export function isCommandClass(value: unknown): value is CommandClass {
|
|
273
|
+
if (typeof value !== 'function') return false
|
|
274
|
+
const candidate = value as Partial<CommandClass>
|
|
275
|
+
return (
|
|
276
|
+
typeof candidate.commandName === 'string' &&
|
|
277
|
+
candidate.commandName.length > 0 &&
|
|
278
|
+
typeof candidate.description === 'string'
|
|
279
|
+
)
|
|
280
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console UI — `this.logger` and `this.prompt` on every command (Console parity).
|
|
3
|
+
*
|
|
4
|
+
* Kept dependency-free: the framework ships no colour or prompt library, and
|
|
5
|
+
* pulling one in for the CLI surface alone is not worth the install weight.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { stdout } from 'node:process'
|
|
9
|
+
|
|
10
|
+
/** Colour is opt-out via NO_COLOR and skipped when piped to a file. */
|
|
11
|
+
function supportsColour(stream: NodeJS.WriteStream): boolean {
|
|
12
|
+
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') return false
|
|
13
|
+
if (process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== '') return true
|
|
14
|
+
return Boolean(stream.isTTY)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CODES = {
|
|
18
|
+
reset: '\u001B[0m',
|
|
19
|
+
dim: '\u001B[2m',
|
|
20
|
+
red: '\u001B[31m',
|
|
21
|
+
green: '\u001B[32m',
|
|
22
|
+
yellow: '\u001B[33m',
|
|
23
|
+
blue: '\u001B[34m',
|
|
24
|
+
magenta: '\u001B[35m',
|
|
25
|
+
cyan: '\u001B[36m',
|
|
26
|
+
} as const
|
|
27
|
+
|
|
28
|
+
export type Colour = keyof Omit<typeof CODES, 'reset'>
|
|
29
|
+
|
|
30
|
+
export function colourise(
|
|
31
|
+
text: string,
|
|
32
|
+
colour: Colour,
|
|
33
|
+
stream: NodeJS.WriteStream = stdout,
|
|
34
|
+
): string {
|
|
35
|
+
if (!supportsColour(stream)) return text
|
|
36
|
+
return `${CODES[colour]}${text}${CODES.reset}`
|
|
37
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The helpers Console exposes around commands (`@adonisjs/consoleApp` utils).
|
|
3
|
+
*
|
|
4
|
+
* They exist as exports because a custom loader needs them: validating what a
|
|
5
|
+
* module exported, and reporting a name that resolved to nothing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ReamError } from '../errors/ReamError.js'
|
|
9
|
+
import type { Logger } from './cliui.js'
|
|
10
|
+
import { type CommandClass, isCommandClass, type SerializedCommand } from './types.js'
|
|
11
|
+
|
|
12
|
+
/** Sort command and namespace names the way the listing does. */
|
|
13
|
+
export function sortAlphabetically(prev: string, curr: string): number {
|
|
14
|
+
return prev.localeCompare(curr)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Report a name that resolved to nothing, with the closest matches.
|
|
19
|
+
*
|
|
20
|
+
* Shared by the kernel's unknown-command report and the `help` command: two
|
|
21
|
+
* wordings for the same dead end is how they end up disagreeing.
|
|
22
|
+
*/
|
|
23
|
+
export function renderErrorWithSuggestions(
|
|
24
|
+
logger: Logger,
|
|
25
|
+
message: string,
|
|
26
|
+
suggestions: readonly string[],
|
|
27
|
+
): void {
|
|
28
|
+
logger.error(message)
|
|
29
|
+
if (suggestions.length === 0) return
|
|
30
|
+
process.stderr.write(` Did you mean "${suggestions.slice(0, 4).join('", "')}"?\n`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Assert that a module's export is a command class.
|
|
35
|
+
*
|
|
36
|
+
* `exportPath` names the file, because the useful part of the message is WHICH
|
|
37
|
+
* module exported the wrong thing.
|
|
38
|
+
*/
|
|
39
|
+
export function validateCommand(
|
|
40
|
+
command: unknown,
|
|
41
|
+
exportPath: string,
|
|
42
|
+
): asserts command is CommandClass {
|
|
43
|
+
if (isCommandClass(command)) return
|
|
44
|
+
throw new ReamError('E_CONSOLE_INVALID_COMMAND', `Invalid command exported from ${exportPath}.`, {
|
|
45
|
+
hint: 'Expected a class with a static commandName, a static description and a run() method.',
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The same, for a metadata object a loader published. */
|
|
50
|
+
export function validateCommandMetaData(
|
|
51
|
+
metadata: unknown,
|
|
52
|
+
exportPath: string,
|
|
53
|
+
): asserts metadata is SerializedCommand {
|
|
54
|
+
const invalid = (reason: string): never => {
|
|
55
|
+
throw new ReamError(
|
|
56
|
+
'E_CONSOLE_INVALID_COMMAND_METADATA',
|
|
57
|
+
`Invalid command metadata exported from ${exportPath}: ${reason}.`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof metadata !== 'object' || metadata === null) invalid('expected an object')
|
|
62
|
+
const record = metadata as Record<string, unknown>
|
|
63
|
+
if (typeof record.commandName !== 'string' || record.commandName === '') {
|
|
64
|
+
invalid('"commandName" is missing')
|
|
65
|
+
}
|
|
66
|
+
if (typeof record.description !== 'string') invalid('"description" is missing')
|
|
67
|
+
if (!Array.isArray(record.args)) invalid('"args" is missing')
|
|
68
|
+
if (!Array.isArray(record.flags)) invalid('"flags" is missing')
|
|
69
|
+
}
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import 'reflect-metadata'
|
|
13
|
-
import {
|
|
13
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
14
|
+
import {
|
|
15
|
+
getInjectTokens,
|
|
16
|
+
getMethodInjectTokens,
|
|
17
|
+
getServiceMetadata,
|
|
18
|
+
getServiceRegistry,
|
|
19
|
+
} from '../decorators/Service.js'
|
|
14
20
|
import { didYouMean } from '../errors/FuzzyMatcher.js'
|
|
15
21
|
import { ReamError } from '../errors/ReamError.js'
|
|
16
22
|
import type { Binding, ServiceFactory, ServiceToken } from './types.js'
|
|
@@ -22,6 +28,12 @@ import type { Binding, ServiceFactory, ServiceToken } from './types.js'
|
|
|
22
28
|
*/
|
|
23
29
|
const RESERVED_TOKEN_NAMES = new Set(['__proto__', 'constructor', 'prototype'])
|
|
24
30
|
|
|
31
|
+
/** One resolution chain: the tokens on it, and a set for O(1) membership. */
|
|
32
|
+
interface ResolutionChain {
|
|
33
|
+
stack: string[]
|
|
34
|
+
set: Set<string>
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
export class Container {
|
|
26
38
|
#bindings: Map<string, Binding> = new Map()
|
|
27
39
|
#singletons: Map<string, unknown> = new Map()
|
|
@@ -37,8 +49,16 @@ export class Container {
|
|
|
37
49
|
#aliases: Map<string, ServiceToken> = new Map()
|
|
38
50
|
/** token key → post-resolution callbacks (AdonisJS `container.resolving`). */
|
|
39
51
|
#resolvingHooks: Map<string, Array<(value: unknown) => void | Promise<void>>> = new Map()
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The resolution chain currently being walked, for cycle detection.
|
|
54
|
+
*
|
|
55
|
+
* Scoped to the async context, NOT to the container: `resolve()` is async, so
|
|
56
|
+
* two independent chains interleave, and a stack held on the instance would
|
|
57
|
+
* splice them together and report a cycle neither of them has. Per-container
|
|
58
|
+
* storage keeps a chain that crosses two containers from colliding on a token
|
|
59
|
+
* name they happen to share.
|
|
60
|
+
*/
|
|
61
|
+
readonly #chain = new AsyncLocalStorage<ResolutionChain>()
|
|
42
62
|
|
|
43
63
|
// ─── Explicit bindings ────────────────────────────────────
|
|
44
64
|
|
|
@@ -162,21 +182,30 @@ export class Container {
|
|
|
162
182
|
|
|
163
183
|
const key = this.#tokenToKey(token)
|
|
164
184
|
|
|
165
|
-
|
|
166
|
-
|
|
185
|
+
// Top of a chain: open one, so everything this resolution triggers shares
|
|
186
|
+
// it and nothing outside it does.
|
|
187
|
+
const chain = this.#chain.getStore()
|
|
188
|
+
if (chain === undefined) {
|
|
189
|
+
return this.#chain.run({ stack: [], set: new Set() }, () =>
|
|
190
|
+
this.resolve<T>(token, runtimeValues),
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (chain.set.has(key)) {
|
|
195
|
+
const cycle = [...chain.stack, key].join(' → ')
|
|
167
196
|
throw new ReamError('CIRCULAR_DEPENDENCY', `Circular dependency detected: ${cycle}`, {
|
|
168
197
|
hint: 'Break the cycle by resolving one dependency lazily inside a method (`await container.make(Dep)`) instead of injecting it in the constructor.',
|
|
169
198
|
context: { chain: cycle },
|
|
170
199
|
})
|
|
171
200
|
}
|
|
172
|
-
|
|
173
|
-
|
|
201
|
+
chain.stack.push(key)
|
|
202
|
+
chain.set.add(key)
|
|
174
203
|
|
|
175
204
|
try {
|
|
176
205
|
return await this.#resolveInner<T>(key, token, runtimeValues)
|
|
177
206
|
} finally {
|
|
178
|
-
|
|
179
|
-
|
|
207
|
+
chain.stack.pop()
|
|
208
|
+
chain.set.delete(key)
|
|
180
209
|
}
|
|
181
210
|
}
|
|
182
211
|
|
|
@@ -196,8 +225,16 @@ export class Container {
|
|
|
196
225
|
const target: Ctor = Object.getPrototypeOf(instance).constructor
|
|
197
226
|
const paramTypes: unknown[] =
|
|
198
227
|
Reflect.getMetadata('design:paramtypes', target.prototype, method) ?? []
|
|
199
|
-
|
|
200
|
-
|
|
228
|
+
// `@Inject('token')` on a method parameter — a named binding the reflected
|
|
229
|
+
// type cannot express (an interface, or a class registered under a name).
|
|
230
|
+
const injectTokens = getMethodInjectTokens(target.prototype, method)
|
|
231
|
+
|
|
232
|
+
// `@Inject` counts towards the parameter span too: a method may carry named
|
|
233
|
+
// tokens without `design:paramtypes` being emitted for it, and computing the
|
|
234
|
+
// span from the reflected types alone left the loop at zero iterations —
|
|
235
|
+
// the tokens were read and then never used.
|
|
236
|
+
const injectSpan = injectTokens.size === 0 ? 0 : Math.max(...injectTokens.keys()) + 1
|
|
237
|
+
const len = Math.max(paramTypes.length, runtimeValues?.length ?? 0, injectSpan)
|
|
201
238
|
// Sequential resolution keeps the shared cycle-detection stack consistent.
|
|
202
239
|
const args: unknown[] = []
|
|
203
240
|
for (let index = 0; index < len; index += 1) {
|
|
@@ -206,6 +243,11 @@ export class Container {
|
|
|
206
243
|
args.push(runtimeValues[index])
|
|
207
244
|
continue
|
|
208
245
|
}
|
|
246
|
+
const named = injectTokens.get(index)
|
|
247
|
+
if (named !== undefined) {
|
|
248
|
+
args.push(await this.resolve(named))
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
209
251
|
const type = paramTypes[index]
|
|
210
252
|
args.push(isInjectableClass(type) ? await this.resolve(type) : undefined)
|
|
211
253
|
}
|
|
@@ -261,7 +303,7 @@ export class Container {
|
|
|
261
303
|
async #resolveInner<T>(key: string, token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
|
|
262
304
|
// 1. Check swaps (test overrides) — a swap factory may be async.
|
|
263
305
|
if (this.#overrides.has(key)) {
|
|
264
|
-
const swapped = await this.#overrides.get(key)?.()
|
|
306
|
+
const swapped = await this.#overrides.get(key)?.(this)
|
|
265
307
|
return swapped as T
|
|
266
308
|
}
|
|
267
309
|
|
|
@@ -273,7 +315,7 @@ export class Container {
|
|
|
273
315
|
// 3. Check explicit bindings — the factory may be async.
|
|
274
316
|
const binding = this.#bindings.get(key)
|
|
275
317
|
if (binding) {
|
|
276
|
-
const instance = binding.factory ? await binding.factory() : undefined
|
|
318
|
+
const instance = binding.factory ? await binding.factory(this) : undefined
|
|
277
319
|
if (binding.scope === 'singleton') {
|
|
278
320
|
this.#singletons.set(key, instance)
|
|
279
321
|
}
|
package/src/container/types.ts
CHANGED
|
@@ -17,8 +17,21 @@ export interface ServiceMetadata {
|
|
|
17
17
|
// biome-ignore lint/suspicious/noExplicitAny: IoC container tokens must accept any constructor signature
|
|
18
18
|
export type ServiceToken = (new (...args: any[]) => unknown) | string | symbol
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Resolver handed to a binding factory so it can resolve its own dependencies
|
|
22
|
+
* (AdonisJS parity: `container.singleton(token, async (resolver) =>
|
|
23
|
+
* await resolver.make(Dep))`). The Container itself satisfies this contract.
|
|
24
|
+
*/
|
|
25
|
+
export interface ContainerResolver {
|
|
26
|
+
make<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Factory function for creating service instances. Receives a {@link
|
|
31
|
+
* ContainerResolver} (AdonisJS parity) — the async container means a factory
|
|
32
|
+
* that needs another binding must `await resolver.make(Dep)`.
|
|
33
|
+
*/
|
|
34
|
+
export type ServiceFactory = (resolver: ContainerResolver) => unknown
|
|
22
35
|
|
|
23
36
|
/** Binding entry in the container. */
|
|
24
37
|
export interface Binding {
|
|
@@ -51,11 +51,23 @@ export function Service(options: { scope?: ServiceScope; as?: string } = {}): Cl
|
|
|
51
51
|
* @implements FR16
|
|
52
52
|
*/
|
|
53
53
|
export function Inject(token: ServiceToken): ParameterDecorator {
|
|
54
|
-
return (target,
|
|
54
|
+
return (target, propertyKey, parameterIndex) => {
|
|
55
|
+
// A constructor parameter reports no property key and lands on the class;
|
|
56
|
+
// a method parameter lands on the prototype and MUST be keyed by method
|
|
57
|
+
// name, or every method of the class would share one map and overwrite
|
|
58
|
+
// each other's tokens.
|
|
55
59
|
const existingTokens: Map<number, ServiceToken> =
|
|
56
|
-
|
|
60
|
+
(propertyKey === undefined
|
|
61
|
+
? Reflect.getOwnMetadata(INJECT_METADATA_KEY, target)
|
|
62
|
+
: Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey)) ?? new Map()
|
|
63
|
+
|
|
57
64
|
existingTokens.set(parameterIndex, token)
|
|
58
|
-
|
|
65
|
+
|
|
66
|
+
if (propertyKey === undefined) {
|
|
67
|
+
Reflect.defineMetadata(INJECT_METADATA_KEY, existingTokens, target)
|
|
68
|
+
} else {
|
|
69
|
+
Reflect.defineMetadata(INJECT_METADATA_KEY, existingTokens, target, propertyKey)
|
|
70
|
+
}
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
|
|
@@ -66,6 +78,19 @@ export function getInjectTokens(target: AnyConstructor): Map<number, ServiceToke
|
|
|
66
78
|
return Reflect.getOwnMetadata(INJECT_METADATA_KEY, target) ?? new Map()
|
|
67
79
|
}
|
|
68
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Get named injection tokens for one method's parameters.
|
|
83
|
+
*
|
|
84
|
+
* `target` is the prototype carrying the method — that is where a parameter
|
|
85
|
+
* decorator on a method writes.
|
|
86
|
+
*/
|
|
87
|
+
export function getMethodInjectTokens(
|
|
88
|
+
target: object,
|
|
89
|
+
propertyKey: string | symbol,
|
|
90
|
+
): Map<number, ServiceToken> {
|
|
91
|
+
return Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) ?? new Map()
|
|
92
|
+
}
|
|
93
|
+
|
|
69
94
|
/**
|
|
70
95
|
* @inject() class decorator — marks a controller (or service) for transient IoC resolution.
|
|
71
96
|
*
|
package/src/errors/ReamError.ts
CHANGED
|
@@ -7,6 +7,37 @@
|
|
|
7
7
|
* @implements FR71, FR72, FR73, FR77
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Is this a Ream error, and one of these codes?
|
|
12
|
+
*
|
|
13
|
+
* The framework carries ONE error type with a `code`, because an error can be
|
|
14
|
+
* raised in Rust and rebuilt in TypeScript across the NAPI boundary — see
|
|
15
|
+
* {@link ReamError.fromNapi}. A class per code cannot cross that line, so the
|
|
16
|
+
* check that replaces `instanceof MyError` is this one.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* try {
|
|
21
|
+
* await consoleApp.exec('provision')
|
|
22
|
+
* } catch (error) {
|
|
23
|
+
* if (isReamError(error, 'E_CONSOLE_MISSING_FLAG', 'E_CONSOLE_MISSING_ARGUMENT')) {
|
|
24
|
+
* console.error(error.hint) // narrowed: `error` IS a ReamError here
|
|
25
|
+
* return
|
|
26
|
+
* }
|
|
27
|
+
* throw error
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function isReamError(error: unknown): error is ReamError
|
|
32
|
+
export function isReamError<const Code extends string>(
|
|
33
|
+
error: unknown,
|
|
34
|
+
...codes: [Code, ...Code[]]
|
|
35
|
+
): error is ReamError & { code: Code }
|
|
36
|
+
export function isReamError(error: unknown, ...codes: string[]): boolean {
|
|
37
|
+
if (!(error instanceof ReamError)) return false
|
|
38
|
+
return codes.length === 0 || codes.includes(error.code)
|
|
39
|
+
}
|
|
40
|
+
|
|
10
41
|
export class ReamError extends Error {
|
|
11
42
|
/** Error code (e.g., "ATLAS_QUERY_ERROR", "CONTAINER_NOT_FOUND") */
|
|
12
43
|
readonly code: string
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render an error for a human reading a terminal (AdonisJS `prettyPrintError`).
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than a helper inside `Ignitor.ts`: the console kernel
|
|
5
|
+
* renders the failures of the command it ran, and reaching into the Ignitor for
|
|
6
|
+
* that would make the two import each other.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ReamError } from './ReamError.js'
|
|
10
|
+
|
|
11
|
+
export function prettyPrintError(error: unknown): void {
|
|
12
|
+
if (error instanceof ReamError) {
|
|
13
|
+
console.error(error.toDevString())
|
|
14
|
+
} else if (error instanceof Error) {
|
|
15
|
+
console.error(`\n ${error.message}\n`)
|
|
16
|
+
if (error.stack) console.error(error.stack)
|
|
17
|
+
} else {
|
|
18
|
+
console.error(error)
|
|
19
|
+
}
|
|
20
|
+
}
|