@c9up/ream 0.2.25 → 0.2.27
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 +12 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +114 -23
- package/dist/Codemods.js.map +1 -1
- package/dist/HttpKernel.d.ts +5 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +6 -6
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +87 -14
- 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/prompts.d.ts.map +1 -1
- package/dist/console/prompts.js +34 -46
- package/dist/console/prompts.js.map +1 -1
- package/dist/console/selection.d.ts +54 -0
- package/dist/console/selection.d.ts.map +1 -0
- package/dist/console/selection.js +235 -0
- package/dist/console/selection.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/fullReload.d.ts +35 -10
- package/dist/dev/fullReload.d.ts.map +1 -1
- package/dist/dev/fullReload.js +55 -14
- package/dist/dev/fullReload.js.map +1 -1
- package/dist/dev/hotWatcher.d.ts +19 -3
- package/dist/dev/hotWatcher.d.ts.map +1 -1
- package/dist/dev/hotWatcher.js +168 -18
- package/dist/dev/hotWatcher.js.map +1 -1
- package/dist/dev/readyBanner.d.ts +45 -0
- package/dist/dev/readyBanner.d.ts.map +1 -0
- package/dist/dev/readyBanner.js +72 -0
- package/dist/dev/readyBanner.js.map +1 -0
- package/dist/dev/shortcuts.d.ts +41 -0
- package/dist/dev/shortcuts.d.ts.map +1 -0
- package/dist/dev/shortcuts.js +142 -0
- package/dist/dev/shortcuts.js.map +1 -0
- package/dist/errors/prettyPrintError.d.ts.map +1 -1
- package/dist/errors/prettyPrintError.js +5 -8
- package/dist/errors/prettyPrintError.js.map +1 -1
- package/dist/errors/renderError.d.ts +46 -0
- package/dist/errors/renderError.d.ts.map +1 -0
- package/dist/errors/renderError.js +197 -0
- package/dist/errors/renderError.js.map +1 -0
- package/dist/hot.js +12 -2
- package/dist/hot.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/profiler/MemoryProfileStore.d.ts +23 -0
- package/dist/profiler/MemoryProfileStore.d.ts.map +1 -0
- package/dist/profiler/MemoryProfileStore.js +70 -0
- package/dist/profiler/MemoryProfileStore.js.map +1 -0
- package/dist/profiler/Profiler.d.ts +56 -0
- package/dist/profiler/Profiler.d.ts.map +1 -0
- package/dist/profiler/Profiler.js +162 -0
- package/dist/profiler/Profiler.js.map +1 -0
- package/dist/profiler/collectors.d.ts +105 -0
- package/dist/profiler/collectors.d.ts.map +1 -0
- package/dist/profiler/collectors.js +164 -0
- package/dist/profiler/collectors.js.map +1 -0
- package/dist/profiler/endpoint.d.ts +27 -0
- package/dist/profiler/endpoint.d.ts.map +1 -0
- package/dist/profiler/endpoint.js +79 -0
- package/dist/profiler/endpoint.js.map +1 -0
- package/dist/profiler/index.d.ts +26 -0
- package/dist/profiler/index.d.ts.map +1 -0
- package/dist/profiler/index.js +41 -0
- package/dist/profiler/index.js.map +1 -0
- package/dist/profiler/middleware.d.ts +17 -0
- package/dist/profiler/middleware.d.ts.map +1 -0
- package/dist/profiler/middleware.js +79 -0
- package/dist/profiler/middleware.js.map +1 -0
- package/dist/profiler/render.d.ts +23 -0
- package/dist/profiler/render.d.ts.map +1 -0
- package/dist/profiler/render.js +141 -0
- package/dist/profiler/render.js.map +1 -0
- package/dist/profiler/toolbar.d.ts +34 -0
- package/dist/profiler/toolbar.d.ts.map +1 -0
- package/dist/profiler/toolbar.js +104 -0
- package/dist/profiler/toolbar.js.map +1 -0
- package/dist/profiler/types.d.ts +70 -0
- package/dist/profiler/types.d.ts.map +1 -0
- package/dist/profiler/types.js +11 -0
- package/dist/profiler/types.js.map +1 -0
- package/dist/router/Router.d.ts +11 -0
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +16 -0
- package/dist/router/Router.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 +153 -29
- package/src/HttpKernel.ts +12 -8
- package/src/Ignitor.ts +95 -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/prompts.ts +45 -64
- package/src/console/selection.ts +274 -0
- package/src/console/ui.ts +12 -24
- package/src/dev/fullReload.ts +57 -15
- package/src/dev/hotWatcher.ts +165 -21
- package/src/dev/readyBanner.ts +84 -0
- package/src/dev/shortcuts.ts +158 -0
- package/src/errors/prettyPrintError.ts +5 -5
- package/src/errors/renderError.ts +230 -0
- package/src/hot.ts +12 -2
- package/src/index.ts +23 -3
- package/src/profiler/MemoryProfileStore.ts +75 -0
- package/src/profiler/Profiler.ts +182 -0
- package/src/profiler/collectors.ts +230 -0
- package/src/profiler/endpoint.ts +98 -0
- package/src/profiler/index.ts +56 -0
- package/src/profiler/middleware.ts +83 -0
- package/src/profiler/render.ts +173 -0
- package/src/profiler/toolbar.ts +106 -0
- package/src/profiler/types.ts +75 -0
- package/src/router/Router.ts +16 -0
- 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
package/src/Codemods.ts
CHANGED
|
@@ -1,15 +1,77 @@
|
|
|
1
1
|
import * as fs from 'node:fs'
|
|
2
2
|
import * as path from 'node:path'
|
|
3
|
+
import { render as renderTemplate, type StubState } from './stubs/template.js'
|
|
4
|
+
|
|
5
|
+
export type { StubState }
|
|
3
6
|
|
|
4
7
|
export interface Codemods {
|
|
5
8
|
addProvider(importPath: string): Promise<void>
|
|
6
9
|
addMetaFile(pattern: string, reloadServer?: boolean): Promise<void>
|
|
7
10
|
addEnvVars(vars: Record<string, string>): Promise<void>
|
|
8
11
|
writeFile(filePath: string, content: string, options?: { force?: boolean }): Promise<void>
|
|
12
|
+
makeUsingStub(
|
|
13
|
+
stubsRoot: string,
|
|
14
|
+
stubPath: string,
|
|
15
|
+
state?: StubState,
|
|
16
|
+
options?: { force?: boolean },
|
|
17
|
+
): Promise<GeneratedStub>
|
|
9
18
|
registerCommand(importPath: string): Promise<void>
|
|
10
19
|
registerMiddleware(importPath: string, options?: { tier?: 'server' | 'router' }): Promise<void>
|
|
11
20
|
}
|
|
12
21
|
|
|
22
|
+
/** What a stub may interpolate. Scalars only — a stub is text, not a program. */
|
|
23
|
+
|
|
24
|
+
/** Where a stub landed, and what it wrote there. */
|
|
25
|
+
export interface GeneratedStub {
|
|
26
|
+
/** Relative to the project root. */
|
|
27
|
+
path: string
|
|
28
|
+
contents: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A stub, split into the destination it declares and the body it renders.
|
|
33
|
+
*
|
|
34
|
+
* The format is the one `ream-cli`'s generators already read, deliberately:
|
|
35
|
+
* two stub dialects in one framework is a worse tax than the feature is worth.
|
|
36
|
+
*
|
|
37
|
+
* ---
|
|
38
|
+
* to: config/visa.ts
|
|
39
|
+
* ---
|
|
40
|
+
* export default defineConfig({ ... })
|
|
41
|
+
*
|
|
42
|
+
* Adonis declares the same thing with `{{{ exports({ to: … }) }}}`, which is
|
|
43
|
+
* JavaScript we cannot evaluate here — the front matter is that line, written
|
|
44
|
+
* so a text substitution can read it.
|
|
45
|
+
*/
|
|
46
|
+
function splitFrontMatter(template: string): { to?: string; body: string } {
|
|
47
|
+
const trimmed = template.replace(/^[\uFEFF\r\n]+/, '')
|
|
48
|
+
if (!trimmed.startsWith('---')) return { body: template }
|
|
49
|
+
|
|
50
|
+
const after = trimmed.slice(3).replace(/^\r?\n/, '')
|
|
51
|
+
const end = after.indexOf('\n---')
|
|
52
|
+
if (end === -1) {
|
|
53
|
+
throw new Error('[configure] stub front matter opened with `---` but never closed')
|
|
54
|
+
}
|
|
55
|
+
const block = after.slice(0, end)
|
|
56
|
+
const body = after.slice(end + 4).replace(/^\r?\n/, '')
|
|
57
|
+
|
|
58
|
+
let to: string | undefined
|
|
59
|
+
for (const rawLine of block.split('\n')) {
|
|
60
|
+
const line = rawLine.trim()
|
|
61
|
+
if (line === '' || line.startsWith('#')) continue
|
|
62
|
+
const separator = line.indexOf(':')
|
|
63
|
+
if (separator === -1) {
|
|
64
|
+
throw new Error(`[configure] unreadable stub front-matter line: \`${line}\``)
|
|
65
|
+
}
|
|
66
|
+
const key = line.slice(0, separator).trim()
|
|
67
|
+
const value = line.slice(separator + 1).trim()
|
|
68
|
+
if (key === 'to') to = value
|
|
69
|
+
// Any other key is ignored rather than refused: a stub written for a
|
|
70
|
+
// later version of this loader must not break the current one.
|
|
71
|
+
}
|
|
72
|
+
return to === undefined ? { body } : { to, body }
|
|
73
|
+
}
|
|
74
|
+
|
|
13
75
|
type MiddlewareTier = 'server' | 'router'
|
|
14
76
|
|
|
15
77
|
function isMiddlewareTier(value: unknown): value is MiddlewareTier {
|
|
@@ -70,6 +132,55 @@ export function createCodemods(options?: { force?: boolean; cwd?: string }): Cod
|
|
|
70
132
|
const force = options?.force ?? false
|
|
71
133
|
const root = path.resolve(options?.cwd ?? process.cwd())
|
|
72
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The one place a file reaches the disk.
|
|
137
|
+
*
|
|
138
|
+
* Shared by `writeFile` and `makeUsingStub` rather than copied, because what
|
|
139
|
+
* it does is not writing — it is refusing to write outside the project, to
|
|
140
|
+
* follow a symlink out of it, or over something that already exists.
|
|
141
|
+
*/
|
|
142
|
+
async function writeGuarded(
|
|
143
|
+
filePath: string,
|
|
144
|
+
content: string,
|
|
145
|
+
shouldOverwrite: boolean,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
if (path.isAbsolute(filePath)) {
|
|
148
|
+
throw new Error(`[configure] Absolute paths not allowed: ${filePath}`)
|
|
149
|
+
}
|
|
150
|
+
const resolved = path.resolve(root, filePath)
|
|
151
|
+
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
152
|
+
throw new Error(`[configure] Refusing to write outside project root: ${filePath}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const shouldForce = shouldOverwrite
|
|
156
|
+
if (fs.existsSync(resolved) && !shouldForce) return
|
|
157
|
+
|
|
158
|
+
const dir = path.dirname(resolved)
|
|
159
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
160
|
+
|
|
161
|
+
const canonRoot = fs.realpathSync(root)
|
|
162
|
+
|
|
163
|
+
if (fs.existsSync(dir)) {
|
|
164
|
+
const canonDir = fs.realpathSync(dir)
|
|
165
|
+
if (!canonDir.startsWith(canonRoot + path.sep) && canonDir !== canonRoot) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`[configure] Symlink escape detected: ${filePath} — directory resolves outside project root`,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (fs.existsSync(resolved)) {
|
|
173
|
+
const canonFile = fs.realpathSync(resolved)
|
|
174
|
+
if (!canonFile.startsWith(canonRoot + path.sep)) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`[configure] Symlink escape detected: ${filePath} — file resolves outside project root`,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fs.writeFileSync(resolved, content)
|
|
182
|
+
}
|
|
183
|
+
|
|
73
184
|
return {
|
|
74
185
|
/**
|
|
75
186
|
* Insert a provider import into `reamrc.ts` by string manipulation.
|
|
@@ -192,41 +303,54 @@ export function createCodemods(options?: { force?: boolean; cwd?: string }): Cod
|
|
|
192
303
|
},
|
|
193
304
|
|
|
194
305
|
async writeFile(filePath: string, content: string, opts?: { force?: boolean }): Promise<void> {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
const resolved = path.resolve(root, filePath)
|
|
199
|
-
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
200
|
-
throw new Error(`[configure] Refusing to write outside project root: ${filePath}`)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const shouldForce = opts?.force ?? force
|
|
204
|
-
if (fs.existsSync(resolved) && !shouldForce) return
|
|
205
|
-
|
|
206
|
-
const dir = path.dirname(resolved)
|
|
207
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
306
|
+
return writeGuarded(filePath, content, opts?.force ?? force)
|
|
307
|
+
},
|
|
208
308
|
|
|
209
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Render a stub and write it where the stub says.
|
|
311
|
+
*
|
|
312
|
+
* The application's own copy wins: a stub published to `stubs/<stubPath>`
|
|
313
|
+
* is used instead of the package's, which is what lets someone change the
|
|
314
|
+
* config a package generates without forking it. Adonis calls the same
|
|
315
|
+
* thing "searches in publishTarget first"; `ream-cli` already does it for
|
|
316
|
+
* the `make:` generators, and this is the same rule on the configure side.
|
|
317
|
+
*
|
|
318
|
+
* A package therefore names its stub after itself — `config/visa.stub`,
|
|
319
|
+
* not `config.stub` — or two packages would publish to the same path.
|
|
320
|
+
*/
|
|
321
|
+
async makeUsingStub(
|
|
322
|
+
stubsRoot: string,
|
|
323
|
+
stubPath: string,
|
|
324
|
+
state: StubState = {},
|
|
325
|
+
opts?: { force?: boolean },
|
|
326
|
+
): Promise<GeneratedStub> {
|
|
327
|
+
if (stubPath.includes('..') || path.isAbsolute(stubPath)) {
|
|
328
|
+
throw new Error(`[configure] Unsafe stub path: ${stubPath}`)
|
|
329
|
+
}
|
|
210
330
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)
|
|
217
|
-
}
|
|
331
|
+
const published = path.resolve(root, 'stubs', stubPath)
|
|
332
|
+
const shipped = path.resolve(stubsRoot, stubPath)
|
|
333
|
+
const source = fs.existsSync(published) ? published : shipped
|
|
334
|
+
if (!fs.existsSync(source)) {
|
|
335
|
+
throw new Error(`[configure] Stub not found: ${stubPath} (looked in ${stubsRoot})`)
|
|
218
336
|
}
|
|
219
337
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
338
|
+
const raw = fs.readFileSync(source, 'utf8')
|
|
339
|
+
const { to, body } = splitFrontMatter(raw)
|
|
340
|
+
if (to === undefined) {
|
|
341
|
+
// Without it there is nowhere to put the file, and guessing from the
|
|
342
|
+
// stub's own name would put a package's config wherever it was named.
|
|
343
|
+
throw new Error(
|
|
344
|
+
`[configure] Stub ${stubPath} declares no destination — add a \`to:\` front matter line.`,
|
|
345
|
+
)
|
|
227
346
|
}
|
|
228
347
|
|
|
229
|
-
|
|
348
|
+
// The destination goes through the engine too: `to: config/{{ name }}.ts`
|
|
349
|
+
// is the reason a single stub can serve several outputs.
|
|
350
|
+
const destination = renderTemplate(to, state)
|
|
351
|
+
const contents = renderTemplate(body, state)
|
|
352
|
+
await writeGuarded(destination, contents, opts?.force ?? force)
|
|
353
|
+
return { path: destination, contents }
|
|
230
354
|
},
|
|
231
355
|
|
|
232
356
|
/**
|
package/src/HttpKernel.ts
CHANGED
|
@@ -41,8 +41,12 @@ export interface HttpKernelConfig {
|
|
|
41
41
|
* handler produced. It lives here rather than in a middleware because this
|
|
42
42
|
* is the one place every response passes through on its way out, whichever
|
|
43
43
|
* route or error handler produced it.
|
|
44
|
+
*
|
|
45
|
+
* It is handed the whole context, not just the nonce: the debug toolbar
|
|
46
|
+
* carries the profile token inline, and that token is only on the response
|
|
47
|
+
* by the time the body is serialised.
|
|
44
48
|
*/
|
|
45
|
-
|
|
49
|
+
devScript?: (ctx: HttpContext) => string
|
|
46
50
|
routerMiddleware?: MiddlewareFunction[]
|
|
47
51
|
onError?: (error: unknown, ctx: HttpContext) => void
|
|
48
52
|
debug?: boolean
|
|
@@ -375,7 +379,7 @@ export function createHttpKernel(
|
|
|
375
379
|
correlationId,
|
|
376
380
|
)
|
|
377
381
|
}
|
|
378
|
-
const serialized = await serializeResponse(ctx, config.
|
|
382
|
+
const serialized = await serializeResponse(ctx, config.devScript)
|
|
379
383
|
// The body is built; anything registered with `response.onFinish()` runs
|
|
380
384
|
// now — after the answer is ready, before we hand it back.
|
|
381
385
|
ctx.response.runFinishCallbacks()
|
|
@@ -431,7 +435,7 @@ export function createHttpKernel(
|
|
|
431
435
|
correlationId,
|
|
432
436
|
)
|
|
433
437
|
}
|
|
434
|
-
const serialized = await serializeResponse(ctx, config.
|
|
438
|
+
const serialized = await serializeResponse(ctx, config.devScript)
|
|
435
439
|
// The body is built; anything registered with `response.onFinish()` runs
|
|
436
440
|
// now — after the answer is ready, before we hand it back.
|
|
437
441
|
ctx.response.runFinishCallbacks()
|
|
@@ -484,18 +488,18 @@ function readControllerGuardMetadata(
|
|
|
484
488
|
|
|
485
489
|
async function serializeResponse(
|
|
486
490
|
ctx: HttpContext,
|
|
487
|
-
|
|
491
|
+
devScript?: (ctx: HttpContext) => string,
|
|
488
492
|
): Promise<HttpKernelResponse> {
|
|
489
493
|
// The terminal step, as AdonisJS' server calls `response.finish()`: it
|
|
490
494
|
// stamps the request id and waits for a body still being produced.
|
|
491
495
|
await ctx.response.finish()
|
|
492
496
|
const streamId = ctx.response.getStreamId()
|
|
493
497
|
const headers = ctx.response.getHeaders()
|
|
494
|
-
const body =
|
|
498
|
+
const body = withDevScript(
|
|
495
499
|
ctx.response.getBody(),
|
|
496
500
|
headers,
|
|
497
501
|
streamId,
|
|
498
|
-
|
|
502
|
+
devScript === undefined ? undefined : devScript(ctx),
|
|
499
503
|
)
|
|
500
504
|
return {
|
|
501
505
|
status: ctx.response.getStatus(),
|
|
@@ -548,14 +552,14 @@ function reconcileRepresentation(
|
|
|
548
552
|
}
|
|
549
553
|
|
|
550
554
|
/**
|
|
551
|
-
* Append the
|
|
555
|
+
* Append the development script to an HTML body.
|
|
552
556
|
*
|
|
553
557
|
* Only HTML, and never a stream: appending to an SSE or a download would
|
|
554
558
|
* corrupt it, and a page is the only thing that can act on the script anyway.
|
|
555
559
|
* Before `</body>` when there is one, so the script is inside the document
|
|
556
560
|
* rather than trailing after it.
|
|
557
561
|
*/
|
|
558
|
-
function
|
|
562
|
+
function withDevScript(
|
|
559
563
|
body: string,
|
|
560
564
|
headers: Record<string, string>,
|
|
561
565
|
streamId: string | undefined,
|
package/src/Ignitor.ts
CHANGED
|
@@ -18,6 +18,8 @@ import type { Console } from './console/Console.js'
|
|
|
18
18
|
import type { CommandLoader, Kernel as ConsoleKernelInstance } from './console/Kernel.js'
|
|
19
19
|
import { type CommandClass, isCommandClass } from './console/types.js'
|
|
20
20
|
import { hmrClientScript, hmrEndpoint } from './dev/hmr.js'
|
|
21
|
+
import { devServerUrl, inDevServer, printReadyBanner } from './dev/readyBanner.js'
|
|
22
|
+
import { installShortcuts } from './dev/shortcuts.js'
|
|
21
23
|
import type { DirectoriesNode } from './directories.js'
|
|
22
24
|
import type { ErrorEvent } from './ErrorBoundary.js'
|
|
23
25
|
import { ErrorBoundary } from './ErrorBoundary.js'
|
|
@@ -38,6 +40,14 @@ import { MiddlewareRegistry } from './middleware/Pipeline.js'
|
|
|
38
40
|
import { MigrationRegistry } from './migrations/MigrationRegistry.js'
|
|
39
41
|
import type { AppContext, ProviderContract } from './Provider.js'
|
|
40
42
|
import { callProviderPhase } from './Provider.js'
|
|
43
|
+
import {
|
|
44
|
+
createProfiler,
|
|
45
|
+
PROFILER_TOKEN_HEADER,
|
|
46
|
+
type Profiler,
|
|
47
|
+
profileRequest,
|
|
48
|
+
profilerEndpoint,
|
|
49
|
+
toolbarScript,
|
|
50
|
+
} from './profiler/index.js'
|
|
41
51
|
import { Router } from './router/Router.js'
|
|
42
52
|
import { CookieSigner } from './security/CookieSigner.js'
|
|
43
53
|
import { SignedUrl } from './security/SignedUrl.js'
|
|
@@ -345,6 +355,13 @@ export class Ignitor {
|
|
|
345
355
|
#hotReloadCleanup?: () => void
|
|
346
356
|
#shutdownHandle?: ShutdownHandle
|
|
347
357
|
#host?: string
|
|
358
|
+
/**
|
|
359
|
+
* Stops listening for the dev-server keys. Only ever set under `ream dev`.
|
|
360
|
+
*
|
|
361
|
+
* Removed on shutdown, or a terminal left in raw mode would swallow the
|
|
362
|
+
* next command you type after the server exits.
|
|
363
|
+
*/
|
|
364
|
+
#removeShortcuts?: () => void
|
|
348
365
|
#console?: Console
|
|
349
366
|
|
|
350
367
|
// Inline configuration (for simple use or testing)
|
|
@@ -812,6 +829,10 @@ export class Ignitor {
|
|
|
812
829
|
// Boot the Server (resolves lazy error handler etc.)
|
|
813
830
|
await this.#server.boot()
|
|
814
831
|
|
|
832
|
+
// Before the kernel: the kernel's server middleware and its dev script
|
|
833
|
+
// both read it.
|
|
834
|
+
await this.#startProfiler()
|
|
835
|
+
|
|
815
836
|
// Start HTTP server if this process serves one
|
|
816
837
|
if (this.#servesHttp() && this.#config.serverFactory) {
|
|
817
838
|
// Build the HttpKernel with server middleware + router middleware.
|
|
@@ -830,7 +851,7 @@ export class Ignitor {
|
|
|
830
851
|
...this.#server.getServerMiddleware(),
|
|
831
852
|
],
|
|
832
853
|
routerMiddleware: this.#router.getRouterMiddleware(),
|
|
833
|
-
|
|
854
|
+
devScript: this.#devScript(),
|
|
834
855
|
onError: (error, ctx) => {
|
|
835
856
|
this.#errorBoundary.serviceError('HttpKernel', error, ctx.id)
|
|
836
857
|
},
|
|
@@ -992,6 +1013,23 @@ export class Ignitor {
|
|
|
992
1013
|
// balancer at a process that cannot answer.
|
|
993
1014
|
this.#app.markReady()
|
|
994
1015
|
this.#phase = 'ready'
|
|
1016
|
+
|
|
1017
|
+
// The address, once there is one to print. Upstream's dev server prints
|
|
1018
|
+
// this sticker when the child reports ready; here the child prints it,
|
|
1019
|
+
// because a port found by scanning past a taken 3000 is known only to the
|
|
1020
|
+
// process that bound it.
|
|
1021
|
+
if (inDevServer() && this.#host !== undefined && this.#_httpServer !== undefined) {
|
|
1022
|
+
const port = await this.#_httpServer.port()
|
|
1023
|
+
printReadyBanner({
|
|
1024
|
+
host: this.#host,
|
|
1025
|
+
port,
|
|
1026
|
+
mode: 'HMR',
|
|
1027
|
+
bootMs: performance.now(),
|
|
1028
|
+
})
|
|
1029
|
+
// The keys upstream's dev server answers to. Installed here for the same
|
|
1030
|
+
// reason the sticker is printed here: `o` needs the address.
|
|
1031
|
+
this.#removeShortcuts = installShortcuts({ url: devServerUrl(this.#host, port) })
|
|
1032
|
+
}
|
|
995
1033
|
}
|
|
996
1034
|
|
|
997
1035
|
/**
|
|
@@ -1154,6 +1192,11 @@ export class Ignitor {
|
|
|
1154
1192
|
async stop(): Promise<void> {
|
|
1155
1193
|
const errors: unknown[] = []
|
|
1156
1194
|
|
|
1195
|
+
// First: a terminal left in raw mode swallows the next command you type
|
|
1196
|
+
// after the server exits, and that outlives the process.
|
|
1197
|
+
this.#removeShortcuts?.()
|
|
1198
|
+
this.#removeShortcuts = undefined
|
|
1199
|
+
|
|
1157
1200
|
// Release the consoleApp locator first — ownership-guarded, so a second Ignitor
|
|
1158
1201
|
// having rebound it is left alone.
|
|
1159
1202
|
if (this.#console !== undefined) {
|
|
@@ -1296,7 +1339,7 @@ export class Ignitor {
|
|
|
1296
1339
|
this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
|
|
1297
1340
|
serverMiddleware: this.#server.getServerMiddleware(),
|
|
1298
1341
|
routerMiddleware: this.#router.getRouterMiddleware(),
|
|
1299
|
-
|
|
1342
|
+
devScript: this.#devScript(),
|
|
1300
1343
|
})
|
|
1301
1344
|
return this.#kernel
|
|
1302
1345
|
}
|
|
@@ -1308,29 +1351,67 @@ export class Ignitor {
|
|
|
1308
1351
|
}
|
|
1309
1352
|
|
|
1310
1353
|
/**
|
|
1311
|
-
*
|
|
1354
|
+
* What development appends to an HTML page: the reload client, and the
|
|
1355
|
+
* debug toolbar once a profile exists for the request.
|
|
1312
1356
|
*
|
|
1313
1357
|
* Gated on the environment rather than on `hot-hook` being present: the
|
|
1314
|
-
* script is just as useful without it, since a restart is exactly
|
|
1315
|
-
* detects. In production
|
|
1358
|
+
* reload script is just as useful without it, since a restart is exactly
|
|
1359
|
+
* what it detects. In production this is `undefined` and the response is
|
|
1360
|
+
* untouched.
|
|
1361
|
+
*
|
|
1362
|
+
* The token is read back off the response rather than passed along a side
|
|
1363
|
+
* channel — the profiling middleware has already stamped it there, which is
|
|
1364
|
+
* also where a client outside the page reads it (Symfony's `X-Debug-Token`).
|
|
1316
1365
|
*/
|
|
1317
|
-
#
|
|
1318
|
-
|
|
1366
|
+
#devScript(): ((ctx: HttpContext) => string) | undefined {
|
|
1367
|
+
if (!this.isDevMode()) return undefined
|
|
1368
|
+
return (ctx) => {
|
|
1369
|
+
const nonce = ctx.response.nonce
|
|
1370
|
+
const reload = hmrClientScript(nonce)
|
|
1371
|
+
const token = ctx.response.getHeader(PROFILER_TOKEN_HEADER)
|
|
1372
|
+
return token === undefined ? reload : reload + toolbarScript(token, nonce)
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* The profiler, in development only.
|
|
1378
|
+
*
|
|
1379
|
+
* It needs the application's emitter — `db:query` is where the database
|
|
1380
|
+
* panel comes from — so it is built here rather than in a field initialiser,
|
|
1381
|
+
* once the container has one. An application without an event bus still gets
|
|
1382
|
+
* every other panel.
|
|
1383
|
+
*/
|
|
1384
|
+
async #startProfiler(): Promise<void> {
|
|
1385
|
+
if (!this.isDevMode() || this.#profiler !== undefined) return
|
|
1386
|
+
const events = this.#app.container.has('events')
|
|
1387
|
+
? await this.#app.container.resolve<Emitter>('events')
|
|
1388
|
+
: { on: () => undefined }
|
|
1389
|
+
this.#profiler = createProfiler(events)
|
|
1319
1390
|
}
|
|
1320
1391
|
|
|
1392
|
+
#profiler?: Profiler
|
|
1393
|
+
|
|
1321
1394
|
/**
|
|
1322
1395
|
* The server middleware the framework itself contributes, ahead of the
|
|
1323
1396
|
* application's.
|
|
1324
1397
|
*
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1398
|
+
* Development only, and the order is the point:
|
|
1399
|
+
*
|
|
1400
|
+
* 1. the profiler's own pages, which must not appear in their own history;
|
|
1401
|
+
* 2. the hot-reload stream, which answers first because it arrives once a
|
|
1402
|
+
* second per open tab and its content depends on nothing — no user, no
|
|
1403
|
+
* session, no body. Behind an application's middleware it paid for the
|
|
1404
|
+
* whole stack each time, which for anything that resolves a user from a
|
|
1405
|
+
* cookie meant a `SELECT` per second for a string holding a process id
|
|
1406
|
+
* and a counter. It is also open for as long as the tab is, so profiling
|
|
1407
|
+
* it would leave one profile unfinished forever;
|
|
1408
|
+
* 3. everything else, wrapped in a profile.
|
|
1331
1409
|
*/
|
|
1332
1410
|
#frameworkServerMiddleware(): MiddlewareFunction[] {
|
|
1333
|
-
|
|
1411
|
+
if (!this.isDevMode()) return []
|
|
1412
|
+
const profiler = this.#profiler
|
|
1413
|
+
if (profiler === undefined) return [hmrEndpoint()]
|
|
1414
|
+
return [profilerEndpoint(profiler), hmrEndpoint(), profileRequest(profiler)]
|
|
1334
1415
|
}
|
|
1335
1416
|
|
|
1336
1417
|
getPhase(): string {
|
package/src/console/Kernel.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { buildFlag } from './decorators.js'
|
|
|
18
18
|
import { ExceptionHandler } from './ExceptionHandler.js'
|
|
19
19
|
import HelpCommand from './HelpCommand.js'
|
|
20
20
|
import ListCommand from './ListCommand.js'
|
|
21
|
+
import { MAKE_COMMANDS } from './make/commands.js'
|
|
21
22
|
import { assignParsedValues, parseArgv, validateParsed } from './parser.js'
|
|
22
23
|
import { Prompt } from './prompts.js'
|
|
23
24
|
import { commandExec } from './tracing.js'
|
|
@@ -229,6 +230,14 @@ export class Kernel {
|
|
|
229
230
|
this.register(HelpCommand)
|
|
230
231
|
this.#defaults.add(HelpCommand.commandName)
|
|
231
232
|
|
|
233
|
+
// The generators. Registered as defaults, so an application that defines
|
|
234
|
+
// its own `make:controller` replaces this one rather than colliding with
|
|
235
|
+
// it — the same standing an app has over `list` and `help`.
|
|
236
|
+
for (const command of MAKE_COMMANDS) {
|
|
237
|
+
this.register(command)
|
|
238
|
+
this.#defaults.add(command.commandName)
|
|
239
|
+
}
|
|
240
|
+
|
|
232
241
|
// Console's `--help`, a global flag whose listener runs the help command.
|
|
233
242
|
this.defineFlag('help', {
|
|
234
243
|
type: 'boolean',
|
|
@@ -460,7 +469,18 @@ export class Kernel {
|
|
|
460
469
|
for (const metadata of await loader.getMetaData()) {
|
|
461
470
|
// Metadata only — the class is imported by `find()`, when someone asks
|
|
462
471
|
// for the command. `loading` / `loaded` belong there too.
|
|
463
|
-
|
|
472
|
+
//
|
|
473
|
+
// A name the kernel registered ITSELF gives way: `list`, `help` and
|
|
474
|
+
// the generators are defaults, and an application that ships its own
|
|
475
|
+
// `make:controller` must get it. Without this the application declared
|
|
476
|
+
// a command that silently never ran — the same rule `register()`
|
|
477
|
+
// applies, applied on the path an application actually uses.
|
|
478
|
+
const isDefault = this.#defaults.has(metadata.commandName)
|
|
479
|
+
if (!this.#commands.has(metadata.commandName) || isDefault) {
|
|
480
|
+
if (isDefault) {
|
|
481
|
+
this.#commands.delete(metadata.commandName)
|
|
482
|
+
this.#defaults.delete(metadata.commandName)
|
|
483
|
+
}
|
|
464
484
|
this.#pending.set(metadata.commandName, { metadata, loader })
|
|
465
485
|
}
|
|
466
486
|
// The metadata is what a manifest-style loader publishes; its aliases
|