@c9up/ream 0.2.24 → 0.2.26
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 +2 -0
- package/dist/Codemods.d.ts +13 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +158 -23
- package/dist/Codemods.js.map +1 -1
- package/dist/Ignitor.d.ts +28 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +15 -15
- package/dist/Ignitor.js.map +1 -1
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +20 -1
- package/dist/console/Kernel.js.map +1 -1
- package/dist/console/cliui.d.ts +9 -245
- package/dist/console/cliui.d.ts.map +1 -1
- package/dist/console/cliui.js +9 -583
- package/dist/console/cliui.js.map +1 -1
- package/dist/console/make/commands.d.ts +19 -0
- package/dist/console/make/commands.d.ts.map +1 -0
- package/dist/console/make/commands.js +252 -0
- package/dist/console/make/commands.js.map +1 -0
- package/dist/console/make/eject.d.ts +28 -0
- package/dist/console/make/eject.d.ts.map +1 -0
- package/dist/console/make/eject.js +161 -0
- package/dist/console/make/eject.js.map +1 -0
- package/dist/console/make/flush.d.ts +51 -0
- package/dist/console/make/flush.d.ts.map +1 -0
- package/dist/console/make/flush.js +93 -0
- package/dist/console/make/flush.js.map +1 -0
- package/dist/console/make/generators.d.ts +36 -0
- package/dist/console/make/generators.d.ts.map +1 -0
- package/dist/console/make/generators.js +148 -0
- package/dist/console/make/generators.js.map +1 -0
- package/dist/console/make/naming.d.ts +32 -0
- package/dist/console/make/naming.d.ts.map +1 -0
- package/dist/console/make/naming.js +82 -0
- package/dist/console/make/naming.js.map +1 -0
- package/dist/console/make/resolve.d.ts +46 -0
- package/dist/console/make/resolve.d.ts.map +1 -0
- package/dist/console/make/resolve.js +113 -0
- package/dist/console/make/resolve.js.map +1 -0
- package/dist/console/ui.d.ts +9 -15
- package/dist/console/ui.d.ts.map +1 -1
- package/dist/console/ui.js +11 -24
- package/dist/console/ui.js.map +1 -1
- package/dist/dev/hmr.d.ts +52 -4
- package/dist/dev/hmr.d.ts.map +1 -1
- package/dist/dev/hmr.js +75 -9
- package/dist/dev/hmr.js.map +1 -1
- package/dist/dev/hotGlob.d.ts +10 -3
- package/dist/dev/hotGlob.d.ts.map +1 -1
- package/dist/dev/hotGlob.js +28 -3
- package/dist/dev/hotGlob.js.map +1 -1
- package/dist/hot.js +56 -3
- package/dist/hot.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/stubs/index.d.ts +11 -0
- package/dist/stubs/index.d.ts.map +1 -0
- package/dist/stubs/index.js +12 -0
- package/dist/stubs/index.js.map +1 -0
- package/dist/stubs/template.d.ts +63 -0
- package/dist/stubs/template.d.ts.map +1 -0
- package/dist/stubs/template.js +209 -0
- package/dist/stubs/template.js.map +1 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +7 -1
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Codemods.ts +206 -29
- package/src/Ignitor.ts +44 -14
- package/src/console/Kernel.ts +21 -1
- package/src/console/cliui.ts +44 -754
- package/src/console/make/commands.ts +270 -0
- package/src/console/make/eject.ts +163 -0
- package/src/console/make/flush.ts +125 -0
- package/src/console/make/generators.ts +176 -0
- package/src/console/make/naming.ts +85 -0
- package/src/console/make/resolve.ts +134 -0
- package/src/console/ui.ts +12 -24
- package/src/dev/hmr.ts +102 -9
- package/src/dev/hotGlob.ts +28 -3
- package/src/hot.ts +59 -3
- package/src/index.ts +17 -3
- package/src/stubs/index.ts +14 -0
- package/src/stubs/template.ts +262 -0
- package/stubs/make/command.stub +29 -0
- package/stubs/make/controller.stub +30 -0
- package/stubs/make/entity.stub +11 -0
- package/stubs/make/event.stub +13 -0
- package/stubs/make/listener.stub +13 -0
- package/stubs/make/middleware.stub +16 -0
- package/stubs/make/provider.stub +26 -0
- package/stubs/make/service.stub +27 -0
- package/stubs/make/validator.stub +10 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `make:` commands.
|
|
3
|
+
*
|
|
4
|
+
* They used to live in the `ream` binary, which meant Rust had to render their
|
|
5
|
+
* stubs — and rendering a stub means evaluating `{{#if resourceful}}`, which is
|
|
6
|
+
* JavaScript. Two renderers is two behaviours waiting to drift, so the
|
|
7
|
+
* generators moved to where the engine already is, the way `make:migration`
|
|
8
|
+
* moved to atlas.
|
|
9
|
+
*
|
|
10
|
+
* One class per command, built from the generator table: Adonis ships a class
|
|
11
|
+
* per command too, and the table is what keeps ten of them from being ten
|
|
12
|
+
* copies. `--force` and the skip-rather-than-clobber rule are Adonis'.
|
|
13
|
+
* `--dry-run` and `--json` are ours, and they are why `@c9up/ream-mcp` can
|
|
14
|
+
* show a plan before anything is written.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { BaseCommand } from '../BaseCommand.js'
|
|
18
|
+
import { args, flags } from '../decorators.js'
|
|
19
|
+
import type { CommandClass } from '../types.js'
|
|
20
|
+
import Eject from './eject.js'
|
|
21
|
+
import { flush, outcomePayload, type PlannedFile, plannedFile } from './flush.js'
|
|
22
|
+
import { GENERATORS, type Generator, generatorFor } from './generators.js'
|
|
23
|
+
import { nameProblem } from './naming.js'
|
|
24
|
+
import { renderStubFor } from './resolve.js'
|
|
25
|
+
|
|
26
|
+
/** A generator's output, or the message explaining why there is none. */
|
|
27
|
+
type Prepared = { file: PlannedFile; published: boolean } | { error: string }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate the names and render the stub. Shared by the single-kind commands
|
|
31
|
+
* and by `make:module`, so all four files are checked before the first write.
|
|
32
|
+
*/
|
|
33
|
+
function prepare(
|
|
34
|
+
generator: Generator,
|
|
35
|
+
module: string | undefined,
|
|
36
|
+
name: string,
|
|
37
|
+
extra: { stack?: string; event?: string },
|
|
38
|
+
cwd: string,
|
|
39
|
+
): Prepared {
|
|
40
|
+
for (const [label, value] of [
|
|
41
|
+
['module', module],
|
|
42
|
+
['name', name],
|
|
43
|
+
] as const) {
|
|
44
|
+
if (value === undefined) continue
|
|
45
|
+
const problem = nameProblem(value)
|
|
46
|
+
if (problem !== undefined) return { error: `${label} "${value}": ${problem}` }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const state = generator.state(name, {
|
|
50
|
+
...(module === undefined ? {} : { module }),
|
|
51
|
+
...(extra.stack === undefined ? {} : { stack: extra.stack }),
|
|
52
|
+
...(extra.event === undefined ? {} : { event: extra.event }),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const rendered = renderStubFor(generator.kind, state, cwd)
|
|
57
|
+
return {
|
|
58
|
+
file: plannedFile(rendered.path, rendered.contents, cwd),
|
|
59
|
+
published: rendered.published,
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return { error: error instanceof Error ? error.message : String(error) }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Write the plan and report it — as JSON when asked, as Adonis' action lines
|
|
68
|
+
* otherwise. `--json` prints nothing else: its whole contract is that the last
|
|
69
|
+
* line of stdout is the payload, and a stray warning above it is how that
|
|
70
|
+
* breaks.
|
|
71
|
+
*/
|
|
72
|
+
function report(
|
|
73
|
+
command: BaseCommand,
|
|
74
|
+
entries: readonly PlannedFile[],
|
|
75
|
+
warnings: readonly string[],
|
|
76
|
+
options: { cwd: string; dryRun: boolean; force: boolean; json: boolean },
|
|
77
|
+
): void {
|
|
78
|
+
const outcome = flush(entries, warnings, options)
|
|
79
|
+
|
|
80
|
+
if (options.json) {
|
|
81
|
+
process.stdout.write(`${JSON.stringify(outcomePayload(outcome))}\n`)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (outcome.status === 'planned') {
|
|
86
|
+
for (const file of outcome.files) {
|
|
87
|
+
// The path AND the contents: a dry run that showed only the path would
|
|
88
|
+
// not answer the question it is asked.
|
|
89
|
+
command.logger.info(`${file.path}${file.exists ? ' (exists)' : ''} — dry run`)
|
|
90
|
+
command.logger.log(file.content)
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
for (const file of [...outcome.createdFiles, ...outcome.modifiedFiles]) {
|
|
94
|
+
command.logger.action(`create ${file}`).succeeded()
|
|
95
|
+
}
|
|
96
|
+
for (const file of outcome.skippedFiles) {
|
|
97
|
+
command.logger.action(`create ${file}`).skipped('file already exists, use --force')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const warning of warnings) command.logger.warning(warning)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Build the command class for one generator. */
|
|
105
|
+
function makeCommand(generator: Generator): CommandClass {
|
|
106
|
+
class MakeCommand extends BaseCommand {
|
|
107
|
+
static override commandName = generator.commandName
|
|
108
|
+
static override description = generator.description
|
|
109
|
+
// Filesystem only: scaffolding a file must not need a booted application,
|
|
110
|
+
// let alone a reachable database.
|
|
111
|
+
static override options = { startApp: false }
|
|
112
|
+
|
|
113
|
+
// `make:controller billing Invoice` — the module comes first, as the
|
|
114
|
+
// binary took it. Declared on the module-scoped commands only, so the
|
|
115
|
+
// others keep a single argument.
|
|
116
|
+
@args.string({
|
|
117
|
+
description: 'Module directory under app/',
|
|
118
|
+
required: generator.moduleScoped,
|
|
119
|
+
})
|
|
120
|
+
declare first: string
|
|
121
|
+
|
|
122
|
+
@args.string({ description: 'Name of the generated file', required: false })
|
|
123
|
+
declare second?: string
|
|
124
|
+
|
|
125
|
+
@flags.boolean({
|
|
126
|
+
description: 'Forcefully overwrite an existing file',
|
|
127
|
+
alias: 'f',
|
|
128
|
+
})
|
|
129
|
+
declare force?: boolean
|
|
130
|
+
|
|
131
|
+
@flags.boolean({
|
|
132
|
+
description: 'Print what would be written, without writing it',
|
|
133
|
+
})
|
|
134
|
+
declare dryRun?: boolean
|
|
135
|
+
|
|
136
|
+
@flags.boolean({ description: 'Report the outcome as a single JSON object' })
|
|
137
|
+
declare json?: boolean
|
|
138
|
+
|
|
139
|
+
@flags.string({
|
|
140
|
+
description: 'Which stack a middleware registers on: router, server or named',
|
|
141
|
+
})
|
|
142
|
+
declare stack?: string
|
|
143
|
+
|
|
144
|
+
@flags.string({ description: 'Event class this listener handles' })
|
|
145
|
+
declare event?: string
|
|
146
|
+
|
|
147
|
+
async run(): Promise<void> {
|
|
148
|
+
// Two positionals for a module-scoped generator, one for the rest.
|
|
149
|
+
const module = generator.moduleScoped ? this.first : undefined
|
|
150
|
+
const name = generator.moduleScoped ? (this.second ?? '') : this.first
|
|
151
|
+
|
|
152
|
+
if (generator.moduleScoped && this.second === undefined) {
|
|
153
|
+
this.logger.error(`${generator.commandName} takes a module and a name`)
|
|
154
|
+
this.exitCode = 1
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cwd = process.cwd()
|
|
159
|
+
const prepared = prepare(
|
|
160
|
+
generator,
|
|
161
|
+
module,
|
|
162
|
+
name,
|
|
163
|
+
{
|
|
164
|
+
...(this.stack === undefined ? {} : { stack: this.stack }),
|
|
165
|
+
...(this.event === undefined ? {} : { event: this.event }),
|
|
166
|
+
},
|
|
167
|
+
cwd,
|
|
168
|
+
)
|
|
169
|
+
if ('error' in prepared) {
|
|
170
|
+
this.logger.error(prepared.error)
|
|
171
|
+
this.exitCode = 1
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
report(this, [prepared.file], [], {
|
|
176
|
+
cwd,
|
|
177
|
+
dryRun: this.dryRun === true,
|
|
178
|
+
force: this.force === true,
|
|
179
|
+
json: this.json === true,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
if (prepared.published && this.json !== true) {
|
|
183
|
+
// Worth saying: the output came from the project's own stub, so a
|
|
184
|
+
// surprise in it is not a bug in ream.
|
|
185
|
+
this.logger.info(`rendered from stubs/make/${generator.kind}.stub`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The class is anonymous to the debugger otherwise, and a stack trace
|
|
191
|
+
// naming `MakeCommand` ten times helps nobody.
|
|
192
|
+
Object.defineProperty(MakeCommand, 'name', {
|
|
193
|
+
value: `Make${generator.kind.charAt(0).toUpperCase()}${generator.kind.slice(1)}`,
|
|
194
|
+
})
|
|
195
|
+
return MakeCommand
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* `make:module` — an entity, a controller and a validator in one go.
|
|
200
|
+
*
|
|
201
|
+
* NOT the migration the binary also emitted: a migration is atlas' format and
|
|
202
|
+
* atlas' directory, and ream inventing one would produce a file its own
|
|
203
|
+
* `migration:run` never reads. `make:migration` is atlas' command, and this
|
|
204
|
+
* says so rather than guessing.
|
|
205
|
+
*
|
|
206
|
+
* The three are rendered before any is written, so a stub that fails to render
|
|
207
|
+
* leaves nothing half-generated behind.
|
|
208
|
+
*/
|
|
209
|
+
class MakeModule extends BaseCommand {
|
|
210
|
+
static override commandName = 'make:module'
|
|
211
|
+
static override description = 'Generate an entity, a controller and a validator'
|
|
212
|
+
static override options = { startApp: false }
|
|
213
|
+
|
|
214
|
+
@args.string({ description: 'Module directory under app/' })
|
|
215
|
+
declare module: string
|
|
216
|
+
|
|
217
|
+
@args.string({ description: 'Name shared by the generated files' })
|
|
218
|
+
declare name: string
|
|
219
|
+
|
|
220
|
+
@flags.boolean({ description: 'Forcefully overwrite existing files', alias: 'f' })
|
|
221
|
+
declare force?: boolean
|
|
222
|
+
|
|
223
|
+
@flags.boolean({ description: 'Print what would be written, without writing it' })
|
|
224
|
+
declare dryRun?: boolean
|
|
225
|
+
|
|
226
|
+
@flags.boolean({ description: 'Report the outcome as a single JSON object' })
|
|
227
|
+
declare json?: boolean
|
|
228
|
+
|
|
229
|
+
async run(): Promise<void> {
|
|
230
|
+
const cwd = process.cwd()
|
|
231
|
+
const entries: PlannedFile[] = []
|
|
232
|
+
|
|
233
|
+
for (const kind of ['entity', 'controller', 'validator'] as const) {
|
|
234
|
+
const generator = generatorFor(kind)
|
|
235
|
+
if (generator === undefined) {
|
|
236
|
+
this.logger.error(`no generator for "${kind}"`)
|
|
237
|
+
this.exitCode = 1
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
const prepared = prepare(generator, this.module, this.name, {}, cwd)
|
|
241
|
+
if ('error' in prepared) {
|
|
242
|
+
this.logger.error(prepared.error)
|
|
243
|
+
this.exitCode = 1
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
entries.push(prepared.file)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const warnings = [
|
|
250
|
+
// Said, not done: a barrel is the application's file, and appending to it
|
|
251
|
+
// blindly is how an export ends up duplicated.
|
|
252
|
+
`if app/${this.module}/index.ts exists, add the new exports to it yourself`,
|
|
253
|
+
"no migration was generated — that one is atlas': run `ream make:migration`",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
report(this, entries, warnings, {
|
|
257
|
+
cwd,
|
|
258
|
+
dryRun: this.dryRun === true,
|
|
259
|
+
force: this.force === true,
|
|
260
|
+
json: this.json === true,
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Every `make:` command, built once. */
|
|
266
|
+
export const MAKE_COMMANDS: readonly CommandClass[] = [
|
|
267
|
+
...GENERATORS.map(makeCommand),
|
|
268
|
+
MakeModule,
|
|
269
|
+
Eject,
|
|
270
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `eject` — copy a package's stub into the application so it can be edited.
|
|
3
|
+
*
|
|
4
|
+
* The other half of the override rule: `renderStub` prefers `stubs/<path>`
|
|
5
|
+
* when the application has one, and this is how it gets there. Copied
|
|
6
|
+
* verbatim, front matter included, because the `to:` line is part of what a
|
|
7
|
+
* project may want to change.
|
|
8
|
+
*
|
|
9
|
+
* Adonis' command, name and argument included — `node ace eject make/controller
|
|
10
|
+
* --pkg=@adonisjs/lucid`. `--pkg` reads the package's own `stubsRoot`, which
|
|
11
|
+
* every Ream package exports from its `./stubs` subpath, so ejecting atlas' or
|
|
12
|
+
* warden's templates needs nothing added here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'node:fs'
|
|
16
|
+
import * as path from 'node:path'
|
|
17
|
+
import { BaseCommand } from '../BaseCommand.js'
|
|
18
|
+
import { args, flags } from '../decorators.js'
|
|
19
|
+
import { GENERATORS } from './generators.js'
|
|
20
|
+
import {
|
|
21
|
+
isSafeStubPath,
|
|
22
|
+
PUBLISHED_ROOT,
|
|
23
|
+
REAM_STUBS_ROOT,
|
|
24
|
+
readStub,
|
|
25
|
+
splitFrontMatter,
|
|
26
|
+
stubPathFor,
|
|
27
|
+
} from './resolve.js'
|
|
28
|
+
|
|
29
|
+
/** Every `{{ name }}` a stub reads, so the listing cannot advertise a wrong one. */
|
|
30
|
+
function variablesOf(source: string): string[] {
|
|
31
|
+
const found = new Set<string>()
|
|
32
|
+
for (const match of source.matchAll(/\{\{\{?\s*([\w.]+)\s*\}?\}\}/g)) {
|
|
33
|
+
const name = match[1]
|
|
34
|
+
if (name !== undefined) found.add(name)
|
|
35
|
+
}
|
|
36
|
+
return [...found].sort()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The `stubsRoot` a module exports, when it exports one. */
|
|
40
|
+
async function stubsRootFrom(specifier: string): Promise<string | undefined> {
|
|
41
|
+
try {
|
|
42
|
+
const loaded: unknown = await import(specifier)
|
|
43
|
+
if (typeof loaded !== 'object' || loaded === null) return undefined
|
|
44
|
+
const root = (loaded as { stubsRoot?: unknown }).stubsRoot
|
|
45
|
+
return typeof root === 'string' ? root : undefined
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The `stubsRoot` a package exports, or nothing when it exports none.
|
|
53
|
+
*
|
|
54
|
+
* Read from the package rather than guessed from `node_modules/<pkg>/stubs`:
|
|
55
|
+
* a package is free to keep them elsewhere, and only its own export says
|
|
56
|
+
* where. Adonis reads the entrypoint, so that is tried first; the `./stubs`
|
|
57
|
+
* subpath is tried after, because a barrel that also runs in a browser cannot
|
|
58
|
+
* export a filesystem path — which is why Ream packages put it on a subpath.
|
|
59
|
+
*/
|
|
60
|
+
async function stubsRootOf(pkg: string): Promise<string | undefined> {
|
|
61
|
+
if (pkg === '@c9up/ream') return REAM_STUBS_ROOT
|
|
62
|
+
return (await stubsRootFrom(pkg)) ?? (await stubsRootFrom(`${pkg}/stubs`))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Every `.stub` under `root`, as paths relative to it. */
|
|
66
|
+
function stubsUnder(root: string, prefix = ''): string[] {
|
|
67
|
+
const found: string[] = []
|
|
68
|
+
for (const entry of fs.readdirSync(path.join(root, prefix), { withFileTypes: true })) {
|
|
69
|
+
const relative = prefix === '' ? entry.name : `${prefix}/${entry.name}`
|
|
70
|
+
if (entry.isDirectory()) found.push(...stubsUnder(root, relative))
|
|
71
|
+
else if (entry.name.endsWith('.stub')) found.push(relative)
|
|
72
|
+
}
|
|
73
|
+
return found.sort()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default class Eject extends BaseCommand {
|
|
77
|
+
static override commandName = 'eject'
|
|
78
|
+
static override description =
|
|
79
|
+
'Copy a package stub into stubs/ so the project can customise what make: generates'
|
|
80
|
+
static override options = { startApp: false }
|
|
81
|
+
|
|
82
|
+
@args.string({
|
|
83
|
+
description: 'The stub to copy, e.g. make/controller.stub — omit for a directory or all',
|
|
84
|
+
required: false,
|
|
85
|
+
})
|
|
86
|
+
declare stubPath?: string
|
|
87
|
+
|
|
88
|
+
@flags.string({ description: 'Which package to read the stub from' })
|
|
89
|
+
declare pkg?: string
|
|
90
|
+
|
|
91
|
+
@flags.boolean({ description: 'List what can be ejected, and the variables each stub reads' })
|
|
92
|
+
declare list?: boolean
|
|
93
|
+
|
|
94
|
+
@flags.boolean({ description: 'Overwrite a stub the project already ejected' })
|
|
95
|
+
declare force?: boolean
|
|
96
|
+
|
|
97
|
+
async run(): Promise<void> {
|
|
98
|
+
const pkg = this.pkg ?? '@c9up/ream'
|
|
99
|
+
const stubsRoot = await stubsRootOf(pkg)
|
|
100
|
+
if (stubsRoot === undefined || !fs.existsSync(stubsRoot)) {
|
|
101
|
+
this.logger.error(`"${pkg}" ships no stubs (no \`stubsRoot\` on its ./stubs subpath)`)
|
|
102
|
+
this.exitCode = 1
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (this.list === true) {
|
|
107
|
+
this.#printList(stubsRoot)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const requested = this.stubPath
|
|
112
|
+
if (requested !== undefined && !isSafeStubPath(requested)) {
|
|
113
|
+
this.logger.error(`unusable stub path: ${requested}`)
|
|
114
|
+
this.exitCode = 1
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A path with no `.stub` suffix names a directory — `eject make` takes the
|
|
119
|
+
// lot, which is what makes ejecting every generator one command.
|
|
120
|
+
const available = stubsUnder(stubsRoot)
|
|
121
|
+
const selected =
|
|
122
|
+
requested === undefined
|
|
123
|
+
? available
|
|
124
|
+
: requested.endsWith('.stub')
|
|
125
|
+
? available.filter((candidate) => candidate === requested)
|
|
126
|
+
: available.filter((candidate) => candidate.startsWith(`${requested}/`))
|
|
127
|
+
|
|
128
|
+
if (selected.length === 0) {
|
|
129
|
+
this.logger.error(`"${pkg}" ships no stub matching "${requested ?? ''}"`)
|
|
130
|
+
this.exitCode = 1
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const stub of selected) {
|
|
135
|
+
const destination = path.join(process.cwd(), PUBLISHED_ROOT, stub)
|
|
136
|
+
if (fs.existsSync(destination) && this.force !== true) {
|
|
137
|
+
this.logger
|
|
138
|
+
.action(`eject ${PUBLISHED_ROOT}/${stub}`)
|
|
139
|
+
.skipped('already ejected, use --force')
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
|
143
|
+
// The shipped one, never the project's copy: ejecting over your own edit
|
|
144
|
+
// with your own edit is a no-op that reads as success.
|
|
145
|
+
fs.writeFileSync(destination, fs.readFileSync(path.join(stubsRoot, stub)))
|
|
146
|
+
this.logger.action(`eject ${PUBLISHED_ROOT}/${stub}`).succeeded()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** What each generator reads, taken from the stub rather than a kept list. */
|
|
151
|
+
#printList(stubsRoot: string): void {
|
|
152
|
+
const table = this.ui.table().head(['Stub', 'Variables'])
|
|
153
|
+
const generatorStubs = new Set(GENERATORS.map((generator) => stubPathFor(generator.kind)))
|
|
154
|
+
for (const stub of stubsUnder(stubsRoot)) {
|
|
155
|
+
const { source } = readStub(stubsRoot, stub, path.join(process.cwd(), '__none__'))
|
|
156
|
+
const variables = generatorStubs.has(stub)
|
|
157
|
+
? variablesOf(splitFrontMatter(source).body).join(', ')
|
|
158
|
+
: ''
|
|
159
|
+
table.row([stub, variables])
|
|
160
|
+
}
|
|
161
|
+
table.render()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning rendered stubs into files on disk.
|
|
3
|
+
*
|
|
4
|
+
* Split from the commands because the rules are the same for one file and for
|
|
5
|
+
* the three `make:module` emits, and they are not the obvious ones:
|
|
6
|
+
*
|
|
7
|
+
* - A file that is already there is SKIPPED, not overwritten and not an
|
|
8
|
+
* error — Adonis' generators answer `SKIPPED:` and carry on, and `--force`
|
|
9
|
+
* is how one asks for the overwrite.
|
|
10
|
+
* - With `--force`, a failure half-way through puts back what the same run
|
|
11
|
+
* had already written. A generator that leaves a clobbered entity behind
|
|
12
|
+
* after failing on the controller is worse than one that writes nothing.
|
|
13
|
+
* That rollback is ours, not Adonis': its generators never overwrite more
|
|
14
|
+
* than one file per command.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs'
|
|
18
|
+
import * as path from 'node:path'
|
|
19
|
+
|
|
20
|
+
/** One file a generator intends to write. */
|
|
21
|
+
export interface PlannedFile {
|
|
22
|
+
path: string
|
|
23
|
+
content: string
|
|
24
|
+
/** Was something already there? Answered while planning, not after. */
|
|
25
|
+
exists: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FlushOptions {
|
|
29
|
+
cwd: string
|
|
30
|
+
dryRun: boolean
|
|
31
|
+
force: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What a run did, in the shapes `--json` prints — see {@link outcomePayload}.
|
|
36
|
+
*/
|
|
37
|
+
export type FlushOutcome =
|
|
38
|
+
| { status: 'planned'; files: PlannedFile[]; warnings: string[] }
|
|
39
|
+
| {
|
|
40
|
+
status: 'written'
|
|
41
|
+
createdFiles: string[]
|
|
42
|
+
modifiedFiles: string[]
|
|
43
|
+
skippedFiles: string[]
|
|
44
|
+
warnings: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `exists` is read now, not at write time: a plan describes one moment. */
|
|
48
|
+
export function plannedFile(destination: string, content: string, cwd: string): PlannedFile {
|
|
49
|
+
return {
|
|
50
|
+
path: destination,
|
|
51
|
+
content,
|
|
52
|
+
exists: fs.existsSync(path.resolve(cwd, destination)),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function flush(
|
|
57
|
+
entries: readonly PlannedFile[],
|
|
58
|
+
warnings: readonly string[],
|
|
59
|
+
options: FlushOptions,
|
|
60
|
+
): FlushOutcome {
|
|
61
|
+
if (options.dryRun) {
|
|
62
|
+
return { status: 'planned', files: [...entries], warnings: [...warnings] }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const createdFiles: string[] = []
|
|
66
|
+
const modifiedFiles: string[] = []
|
|
67
|
+
const skippedFiles: string[] = []
|
|
68
|
+
/** What each written path held before, so a later failure can put it back. */
|
|
69
|
+
const rollbacks: Array<{ path: string; prior: Buffer | undefined }> = []
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (entry.exists && !options.force) {
|
|
74
|
+
skippedFiles.push(entry.path)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
const target = path.resolve(options.cwd, entry.path)
|
|
78
|
+
const prior = entry.exists ? fs.readFileSync(target) : undefined
|
|
79
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
80
|
+
fs.writeFileSync(target, entry.content)
|
|
81
|
+
rollbacks.push({ path: target, prior })
|
|
82
|
+
if (entry.exists) modifiedFiles.push(entry.path)
|
|
83
|
+
else createdFiles.push(entry.path)
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
rollback(rollbacks)
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { status: 'written', createdFiles, modifiedFiles, skippedFiles, warnings: [...warnings] }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Undo in reverse, and never throw: the caller is already reporting a failure,
|
|
95
|
+
* and a rollback error on top of it buries the one that matters. What could not
|
|
96
|
+
* be undone is named on stderr instead.
|
|
97
|
+
*/
|
|
98
|
+
function rollback(rollbacks: readonly { path: string; prior: Buffer | undefined }[]): void {
|
|
99
|
+
for (const entry of [...rollbacks].reverse()) {
|
|
100
|
+
try {
|
|
101
|
+
if (entry.prior === undefined) fs.rmSync(entry.path, { force: true })
|
|
102
|
+
else fs.writeFileSync(entry.path, entry.prior)
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
105
|
+
process.stderr.write(` rollback: failed to restore ${entry.path}: ${detail}\n`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The outcome as the JSON object `--json` prints, kept in one place because it
|
|
112
|
+
* is a contract with another package rather than a debug dump: `ream-mcp` reads
|
|
113
|
+
* the last line of stdout and dispatches on which shape it is.
|
|
114
|
+
*/
|
|
115
|
+
export function outcomePayload(outcome: FlushOutcome): Record<string, unknown> {
|
|
116
|
+
if (outcome.status === 'planned') {
|
|
117
|
+
return { files: outcome.files, warnings: outcome.warnings }
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
createdFiles: outcome.createdFiles,
|
|
121
|
+
modifiedFiles: outcome.modifiedFiles,
|
|
122
|
+
skippedFiles: outcome.skippedFiles,
|
|
123
|
+
warnings: outcome.warnings,
|
|
124
|
+
}
|
|
125
|
+
}
|