@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
package/src/Ignitor.ts
CHANGED
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { Application } from './Application.js'
|
|
17
|
+
import type { Console } from './console/Console.js'
|
|
18
|
+
import type { CommandLoader, Kernel as ConsoleKernelInstance } from './console/Kernel.js'
|
|
19
|
+
import { type CommandClass, isCommandClass } from './console/types.js'
|
|
17
20
|
import type { ErrorEvent } from './ErrorBoundary.js'
|
|
18
21
|
import { ErrorBoundary } from './ErrorBoundary.js'
|
|
19
22
|
import { loadEnvFiles } from './env/loadEnvFiles.js'
|
|
23
|
+
import { prettyPrintError } from './errors/prettyPrintError.js'
|
|
20
24
|
import { ReamError } from './errors/ReamError.js'
|
|
21
25
|
import type { Emitter } from './events/Emitter.js'
|
|
22
26
|
import type { ShutdownHandle } from './GracefulShutdown.js'
|
|
@@ -59,12 +63,84 @@ export interface ReamrcConfig {
|
|
|
59
63
|
}
|
|
60
64
|
>
|
|
61
65
|
commands?: Array<() => Promise<unknown>>
|
|
66
|
+
/**
|
|
67
|
+
* Command shorthands — Console's `commandsAliases`. The value is the command the
|
|
68
|
+
* alias stands for, flags included:
|
|
69
|
+
* `{ resource: 'make:controller --resource' }`
|
|
70
|
+
*/
|
|
71
|
+
commandsAliases?: Record<string, string>
|
|
62
72
|
modules?: {
|
|
63
73
|
/** Path to the modules directory (relative to app root). Default: './app/modules' */
|
|
64
74
|
path?: string
|
|
65
75
|
/** Auto-loaded files in each module directory. Default: ['routes'] */
|
|
66
76
|
autoload?: string[]
|
|
67
77
|
}
|
|
78
|
+
/** Test suites and runner settings — the `tests` block of adonisrc.ts. */
|
|
79
|
+
tests?: TestsConfig
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One test suite, as declared in the rc file (AdonisJS `tests.suites[]`). */
|
|
83
|
+
export interface TestSuiteConfig {
|
|
84
|
+
/** Suite name — what `ream test <name>` selects. */
|
|
85
|
+
name: string
|
|
86
|
+
/**
|
|
87
|
+
* The suite's files: a glob, several, or a callback returning their URLs
|
|
88
|
+
* (Japa `TestFiles`).
|
|
89
|
+
*/
|
|
90
|
+
files: string | string[] | (() => URL[] | Promise<URL[]>)
|
|
91
|
+
/** Per-test timeout for this suite, in ms. */
|
|
92
|
+
timeout?: number
|
|
93
|
+
/** Extra attempts on failure for this suite. */
|
|
94
|
+
retries?: number
|
|
95
|
+
/**
|
|
96
|
+
* Configure the suite before it runs (Japa `TestSuite.configure`). Receives
|
|
97
|
+
* the same handle as the bootstrap's `configureSuite`, and runs after it.
|
|
98
|
+
*
|
|
99
|
+
* Costs an import of the rc file in every worker, since a function cannot
|
|
100
|
+
* cross a process boundary — `configureSuite` in `tests/bootstrap.ts` does
|
|
101
|
+
* the same job for free, and is what AdonisJS itself uses.
|
|
102
|
+
*/
|
|
103
|
+
configure?: (suite: TestSuiteHandle) => void
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* What a suite's `configure` receives — helix's `SuiteHandle`, re-declared here
|
|
108
|
+
* so the rc file types without ream depending on the runner at type level.
|
|
109
|
+
*/
|
|
110
|
+
export interface TestSuiteHandle {
|
|
111
|
+
readonly name: string
|
|
112
|
+
setup(fn: () => void | Promise<void>): TestSuiteHandle
|
|
113
|
+
teardown(fn: () => void | Promise<void>): TestSuiteHandle
|
|
114
|
+
onTest(callback: (test: unknown) => void): TestSuiteHandle
|
|
115
|
+
onGroup(callback: (group: unknown) => void): TestSuiteHandle
|
|
116
|
+
bail(toggle?: boolean): TestSuiteHandle
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The `tests` block of the rc file — AdonisJS `adonisrc.ts` `tests`, field for
|
|
121
|
+
* field. The runner that consumes it is helix; ream reads the file, exactly as
|
|
122
|
+
* `@adonisjs/core` reads adonisrc and hands the suites to Japa.
|
|
123
|
+
*/
|
|
124
|
+
export interface TestsConfig {
|
|
125
|
+
/** Named suites. `ream test` with no argument runs them all, in order. */
|
|
126
|
+
suites?: TestSuiteConfig[]
|
|
127
|
+
/** Default per-test timeout, in ms. */
|
|
128
|
+
timeout?: number
|
|
129
|
+
/** `process.exit()` once the run ends instead of draining the event loop. */
|
|
130
|
+
forceExit?: boolean
|
|
131
|
+
/**
|
|
132
|
+
* Ream particularity: the bootstrap module's path. AdonisJS hardcodes
|
|
133
|
+
* `tests/bootstrap.ts`; that stays the default here, and this overrides it.
|
|
134
|
+
*/
|
|
135
|
+
bootstrap?: string
|
|
136
|
+
/**
|
|
137
|
+
* Point `@japa/runner/core` at helix's shim in every worker, so official Japa
|
|
138
|
+
* plugins (`@japa/assert`, …) instrument the runner that is actually running.
|
|
139
|
+
*
|
|
140
|
+
* Off by default: redirecting a package specifier is not something to do
|
|
141
|
+
* behind a user's back, and a project with no Japa plugin gains nothing.
|
|
142
|
+
*/
|
|
143
|
+
japaPlugins?: boolean
|
|
68
144
|
}
|
|
69
145
|
|
|
70
146
|
/** defineConfig helper — like AdonisJS defineConfig(). */
|
|
@@ -134,6 +210,7 @@ export class Ignitor {
|
|
|
134
210
|
private phase: 'created' | 'registered' | 'booted' | 'started' | 'ready' | 'shutdown' = 'created'
|
|
135
211
|
private hotReloadCleanup?: () => void
|
|
136
212
|
#shutdownHandle?: ShutdownHandle
|
|
213
|
+
#console?: Console
|
|
137
214
|
|
|
138
215
|
// Inline configuration (for simple use or testing)
|
|
139
216
|
private inlineRoutes?: (router: Router) => void
|
|
@@ -311,6 +388,12 @@ export class Ignitor {
|
|
|
311
388
|
await this.phaseRegister()
|
|
312
389
|
await this.phaseBoot()
|
|
313
390
|
await this.phaseStart()
|
|
391
|
+
// Console documents its `consoleApp` service as available once the application has
|
|
392
|
+
// booted, so the locator is installed here rather than on first use —
|
|
393
|
+
// `import consoleApp from '@c9up/ream/services/console'` must not throw in a running
|
|
394
|
+
// app. Only the façade is built (three small modules); loading the
|
|
395
|
+
// commands themselves stays lazy, inside `consoleApp.boot()`.
|
|
396
|
+
await this.consoleApp()
|
|
314
397
|
await this.phaseReady()
|
|
315
398
|
} catch (err) {
|
|
316
399
|
// A throw mid-boot (e.g. a provider ready() failing AFTER the HTTP port
|
|
@@ -329,7 +412,7 @@ export class Ignitor {
|
|
|
329
412
|
|
|
330
413
|
/**
|
|
331
414
|
* Load `.env` files into `process.env` before config and providers boot —
|
|
332
|
-
* mirroring AdonisJS, which loads env in BOTH the HTTP and the
|
|
415
|
+
* mirroring AdonisJS, which loads env in BOTH the HTTP and the consoleApp (console)
|
|
333
416
|
* flows. Without this, `ream migrate` and other console commands booted in a
|
|
334
417
|
* clean subprocess never saw the `.env` the web server got from the shell.
|
|
335
418
|
*
|
|
@@ -635,6 +718,20 @@ export class Ignitor {
|
|
|
635
718
|
*/
|
|
636
719
|
async stop(): Promise<void> {
|
|
637
720
|
const errors: unknown[] = []
|
|
721
|
+
|
|
722
|
+
// Release the consoleApp locator first — ownership-guarded, so a second Ignitor
|
|
723
|
+
// having rebound it is left alone.
|
|
724
|
+
if (this.#console !== undefined) {
|
|
725
|
+
const consoleApp = this.#console
|
|
726
|
+
this.#console = undefined
|
|
727
|
+
try {
|
|
728
|
+
const { clearConsole } = await import('./services/console.js')
|
|
729
|
+
clearConsole(consoleApp)
|
|
730
|
+
} catch {
|
|
731
|
+
/* the module never loaded — nothing to clear */
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
638
735
|
const attempt = async (step: () => void | Promise<void>): Promise<void> => {
|
|
639
736
|
try {
|
|
640
737
|
await step()
|
|
@@ -683,6 +780,51 @@ export class Ignitor {
|
|
|
683
780
|
return this.app
|
|
684
781
|
}
|
|
685
782
|
|
|
783
|
+
/**
|
|
784
|
+
* The application root passed to `new Ignitor(new URL('../', import.meta.url))`.
|
|
785
|
+
* Undefined for inline/test ignitors created without one.
|
|
786
|
+
*/
|
|
787
|
+
getAppRoot(): URL | undefined {
|
|
788
|
+
return this.appRoot
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Has the application already been started? */
|
|
792
|
+
isStarted(): boolean {
|
|
793
|
+
return this.phase === 'started' || this.phase === 'ready'
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* The `consoleApp` façade — the programmatic console.
|
|
798
|
+
*
|
|
799
|
+
* Built on demand: the console modules are only imported when something
|
|
800
|
+
* actually reaches for them, so an HTTP-only boot does not pay for the CLI.
|
|
801
|
+
*/
|
|
802
|
+
async consoleApp(): Promise<Console> {
|
|
803
|
+
if (this.#console !== undefined) return this.#console
|
|
804
|
+
|
|
805
|
+
const [{ Console }, { setConsole }] = await Promise.all([
|
|
806
|
+
import('./console/Console.js'),
|
|
807
|
+
import('./services/console.js'),
|
|
808
|
+
])
|
|
809
|
+
const { kernel, load } = await new ConsoleKernel(this).build()
|
|
810
|
+
const consoleApp = new Console({ kernel, load })
|
|
811
|
+
|
|
812
|
+
this.#console = consoleApp
|
|
813
|
+
setConsole(consoleApp)
|
|
814
|
+
this.app.container.singleton('console', () => consoleApp)
|
|
815
|
+
return consoleApp
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** The loaded rc file, if `useRcFile()` was called. */
|
|
819
|
+
getRcFile(): ReamrcConfig | undefined {
|
|
820
|
+
return this.reamrc
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** The custom module importer, when the app provided one. */
|
|
824
|
+
getImporter(): ((filePath: string) => Promise<unknown>) | undefined {
|
|
825
|
+
return this.config.importer
|
|
826
|
+
}
|
|
827
|
+
|
|
686
828
|
getRouter(): Router {
|
|
687
829
|
return this.router
|
|
688
830
|
}
|
|
@@ -744,65 +886,221 @@ export class Ignitor {
|
|
|
744
886
|
}
|
|
745
887
|
}
|
|
746
888
|
|
|
747
|
-
|
|
748
|
-
* Pretty-print an error (like AdonisJS prettyPrintError).
|
|
749
|
-
*/
|
|
750
|
-
export function prettyPrintError(error: unknown): void {
|
|
751
|
-
if (error instanceof ReamError) {
|
|
752
|
-
console.error(error.toDevString())
|
|
753
|
-
} else if (error instanceof Error) {
|
|
754
|
-
console.error(`\n ${error.message}\n`)
|
|
755
|
-
if (error.stack) console.error(error.stack)
|
|
756
|
-
} else {
|
|
757
|
-
console.error(error)
|
|
758
|
-
}
|
|
759
|
-
}
|
|
889
|
+
export { prettyPrintError }
|
|
760
890
|
|
|
761
891
|
/**
|
|
762
|
-
* ConsoleKernel —
|
|
763
|
-
*
|
|
892
|
+
* ConsoleKernel — boots the app in console mode and dispatches a command.
|
|
893
|
+
*
|
|
894
|
+
* Console parity: `new Ignitor(...).console().handle(process.argv.slice(2))`.
|
|
895
|
+
*
|
|
896
|
+
* Commands come from two places, as in Console:
|
|
897
|
+
* 1. the app's own `commands/` directory, discovered automatically;
|
|
898
|
+
* 2. `reamrc.commands[]`, which registers commands shipped by packages.
|
|
899
|
+
*
|
|
900
|
+
* The application is booted lazily — only for a command declaring
|
|
901
|
+
* `options.startApp`. A generator has no reason to open a database
|
|
902
|
+
* connection, and paying the boot cost on every `--help` is worse than
|
|
903
|
+
* pointless: it fails on a machine where the DB is simply not running.
|
|
764
904
|
*/
|
|
765
905
|
export class ConsoleKernel {
|
|
766
|
-
|
|
906
|
+
#ignitor: Ignitor
|
|
907
|
+
#started = false
|
|
767
908
|
|
|
768
909
|
constructor(ignitor: Ignitor) {
|
|
769
|
-
this
|
|
910
|
+
this.#ignitor = ignitor
|
|
770
911
|
}
|
|
771
912
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
913
|
+
/**
|
|
914
|
+
* Build the console kernel and the function that fills it.
|
|
915
|
+
*
|
|
916
|
+
* Shared by `handle()` (the CLI) and the `consoleApp` façade (programmatic use), so
|
|
917
|
+
* both see exactly the same commands — discovery plus `reamrc.commands` —
|
|
918
|
+
* instead of two loaders drifting apart.
|
|
919
|
+
*/
|
|
920
|
+
async build(): Promise<{ kernel: ConsoleKernelInstance; load: () => Promise<void> }> {
|
|
921
|
+
const { Kernel } = await import('./console/Kernel.js')
|
|
922
|
+
|
|
923
|
+
const kernel = new Kernel({
|
|
924
|
+
binaryName: 'ream',
|
|
925
|
+
// A `staysAlive` command ends itself through `this.terminate()`; the
|
|
926
|
+
// kernel deliberately does not tear the app down for those.
|
|
927
|
+
onTerminate: async () => {
|
|
928
|
+
if (this.#started) await this.#ignitor.stop()
|
|
929
|
+
},
|
|
930
|
+
startApp: async () => {
|
|
931
|
+
// The façade can be used from an already-running application; starting
|
|
932
|
+
// it a second time would re-register providers.
|
|
933
|
+
if (!this.#ignitor.isStarted()) {
|
|
934
|
+
await this.#ignitor.start()
|
|
935
|
+
this.#started = true
|
|
936
|
+
}
|
|
937
|
+
const app = this.#ignitor.getApp()
|
|
938
|
+
app.container.singleton('consoleKernel', () => kernel)
|
|
939
|
+
return app
|
|
940
|
+
},
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
// Discovery is registered as a LOADER, not run beside the kernel: `boot()`
|
|
944
|
+
// is then the single moment commands come in, whether they were found on
|
|
945
|
+
// disk, declared in the rc file, or added by an application's own loader.
|
|
946
|
+
kernel.addLoader(this.#commandLoader(kernel))
|
|
947
|
+
|
|
948
|
+
const load = async (): Promise<void> => {
|
|
949
|
+
const aliases = this.#ignitor.getRcFile()?.commandsAliases
|
|
950
|
+
if (aliases !== undefined) {
|
|
951
|
+
for (const [alias, expansion] of Object.entries(aliases)) {
|
|
952
|
+
kernel.addAlias(alias, expansion)
|
|
793
953
|
}
|
|
794
954
|
}
|
|
955
|
+
await kernel.boot()
|
|
795
956
|
}
|
|
796
957
|
|
|
797
|
-
|
|
958
|
+
return { kernel, load }
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async handle(argv: string[]): Promise<void> {
|
|
962
|
+
const { kernel, load } = await this.build()
|
|
963
|
+
await load()
|
|
964
|
+
|
|
965
|
+
let staysAlive = false
|
|
798
966
|
try {
|
|
799
|
-
|
|
967
|
+
// `process.execArgv` IS what node was started with — Console's `nodeArgs`,
|
|
968
|
+
// which a command reads to know how the process was launched.
|
|
969
|
+
const result = await kernel.handle(argv, process.execArgv)
|
|
970
|
+
staysAlive = result.staysAlive
|
|
800
971
|
} finally {
|
|
801
972
|
// A throwing command must still release the Ignitor's resources
|
|
802
973
|
// (scheduler tickers, watchers) — otherwise the CLI process hangs.
|
|
803
|
-
|
|
974
|
+
// A `staysAlive` command keeps them on purpose.
|
|
975
|
+
if (this.#started && !staysAlive) await this.#ignitor.stop()
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* The application's commands, as a loader the kernel consumes at boot.
|
|
981
|
+
*
|
|
982
|
+
* Two sources, one collection: the `commands/` directory and the rc file's
|
|
983
|
+
* `commands[]`. Collecting them together is what lets a command listed in
|
|
984
|
+
* both be reported as the duplicate it is.
|
|
985
|
+
*/
|
|
986
|
+
#commandLoader(kernel: ConsoleCommandKernel): CommandLoader {
|
|
987
|
+
const found = new Map<string, CommandClass>()
|
|
988
|
+
|
|
989
|
+
return {
|
|
990
|
+
getMetaData: async () => {
|
|
991
|
+
await this.#discoverAppCommands(kernel, found)
|
|
992
|
+
await this.#loadPackageCommands(found)
|
|
993
|
+
// Imported here, not at the top of the file: the console stack is only
|
|
994
|
+
// loaded for a console dispatch, and a static import would pull it into
|
|
995
|
+
// every HTTP boot.
|
|
996
|
+
const { serializeCommand } = await import('./console/Kernel.js')
|
|
997
|
+
return [...found.values()].map(serializeCommand)
|
|
998
|
+
},
|
|
999
|
+
getCommand: async (metadata) => found.get(metadata.commandName) ?? null,
|
|
804
1000
|
}
|
|
805
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Load every command in the app's `commands/` directory.
|
|
1005
|
+
*
|
|
1006
|
+
* Tolerant by design: a module that does not default-export a command is
|
|
1007
|
+
* reported and skipped rather than aborting the whole CLI. Discovery scans a
|
|
1008
|
+
* conventional directory, so a stray helper file there must not make
|
|
1009
|
+
* `ream list` unusable. Explicitly declared entries (`reamrc.commands`) are
|
|
1010
|
+
* held to a stricter standard — see {@link #loadPackageCommands}.
|
|
1011
|
+
*/
|
|
1012
|
+
async #discoverAppCommands(
|
|
1013
|
+
kernel: ConsoleCommandKernel,
|
|
1014
|
+
found: Map<string, CommandClass>,
|
|
1015
|
+
): Promise<void> {
|
|
1016
|
+
const root = this.#ignitor.getAppRoot()
|
|
1017
|
+
if (root === undefined) return
|
|
1018
|
+
|
|
1019
|
+
// The framework's own `FsLoader`, with the application's importer plugged
|
|
1020
|
+
// in: scanning a directory for commands is written once, and an app that
|
|
1021
|
+
// compiles its sources differently only replaces the import.
|
|
1022
|
+
const { FsLoader } = await import('./console/loaders.js')
|
|
1023
|
+
const importModule = (file: URL): Promise<unknown> => this.#importModule(file)
|
|
1024
|
+
|
|
1025
|
+
const loader = new (class extends FsLoader {
|
|
1026
|
+
protected override import(file: URL): Promise<unknown> {
|
|
1027
|
+
return importModule(file)
|
|
1028
|
+
}
|
|
1029
|
+
})(new URL('commands/', root)).onSkipped((fileName) => {
|
|
1030
|
+
// Tolerant by design: discovery scans a conventional directory, so a
|
|
1031
|
+
// stray helper there must not make `ream list` unusable.
|
|
1032
|
+
kernel.logger.warning(`Skipped ${fileName} in commands/ — no default-exported command class.`)
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
for (const metadata of await loader.getMetaData()) {
|
|
1036
|
+
const command = await loader.getCommand(metadata)
|
|
1037
|
+
if (command !== null) found.set(command.commandName, command)
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Register commands declared in `reamrc.commands[]` — the channel for
|
|
1043
|
+
* commands shipped by packages, which discovery cannot see.
|
|
1044
|
+
*
|
|
1045
|
+
* Strict: an entry listed by hand that does not resolve to a command is a
|
|
1046
|
+
* configuration error, and silently ignoring it is how a command "disappears"
|
|
1047
|
+
* with no explanation.
|
|
1048
|
+
*/
|
|
1049
|
+
async #loadPackageCommands(found: Map<string, CommandClass>): Promise<void> {
|
|
1050
|
+
const declared = this.#ignitor.getRcFile()?.commands
|
|
1051
|
+
if (!declared) return
|
|
1052
|
+
|
|
1053
|
+
for (const entry of declared) {
|
|
1054
|
+
const command = commandOf(await entry())
|
|
1055
|
+
if (command === undefined) {
|
|
1056
|
+
throw new ReamError(
|
|
1057
|
+
'E_CONSOLE_INVALID_COMMAND',
|
|
1058
|
+
'An entry of reamrc.ts `commands` does not default-export a command class.',
|
|
1059
|
+
{
|
|
1060
|
+
hint: 'Expected `export default class X { static commandName = "..."; static description = "..."; run() {} }`.',
|
|
1061
|
+
},
|
|
1062
|
+
)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// The most likely duplicate after the move to auto-discovery: a command
|
|
1066
|
+
// living in `commands/` AND still listed by hand. Say so, rather than
|
|
1067
|
+
// leaving the user to guess which two registrations collided.
|
|
1068
|
+
const existing = found.get(command.commandName)
|
|
1069
|
+
if (existing !== undefined && existing !== command) {
|
|
1070
|
+
throw new ReamError(
|
|
1071
|
+
'E_CONSOLE_DUPLICATE_COMMAND',
|
|
1072
|
+
`Two commands claim the name "${command.commandName}".`,
|
|
1073
|
+
{
|
|
1074
|
+
hint: 'Commands in commands/ are discovered automatically — remove this entry from reamrc.ts `commands`, which is only for commands shipped by packages.',
|
|
1075
|
+
},
|
|
1076
|
+
)
|
|
1077
|
+
}
|
|
1078
|
+
found.set(command.commandName, command)
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** Honour the app's custom importer when it provided one. */
|
|
1083
|
+
async #importModule(file: URL): Promise<unknown> {
|
|
1084
|
+
const importer = this.#ignitor.getImporter()
|
|
1085
|
+
if (importer === undefined) return import(file.href)
|
|
1086
|
+
|
|
1087
|
+
const root = this.#ignitor.getAppRoot()
|
|
1088
|
+
const relative = root === undefined ? file.href : `./${file.href.slice(root.href.length)}`
|
|
1089
|
+
return importer(relative)
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** The slice of the console kernel this class drives. */
|
|
1094
|
+
interface ConsoleCommandKernel {
|
|
1095
|
+
addAlias(alias: string, expansion: string): unknown
|
|
1096
|
+
logger: { warning(message: string): void }
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** The command a module default-exports, or undefined when it exports none. */
|
|
1100
|
+
function commandOf(mod: unknown): CommandClass | undefined {
|
|
1101
|
+
if (typeof mod !== 'object' || mod === null) return undefined
|
|
1102
|
+
const value = Reflect.get(mod, 'default')
|
|
1103
|
+
return isCommandClass(value) ? value : undefined
|
|
806
1104
|
}
|
|
807
1105
|
|
|
808
1106
|
async function findAvailablePort(desired: number): Promise<number> {
|