@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the dev server says once it is listening.
|
|
3
|
+
*
|
|
4
|
+
* Upstream prints a sticker the moment the child reports ready: the address,
|
|
5
|
+
* the mode, how long the boot took. Read off `@adonisjs/assembler`'s build:
|
|
6
|
+
*
|
|
7
|
+
* this.ui.sticker()
|
|
8
|
+
* .add(`Server address: ${colors.cyan(serverUrl)}`)
|
|
9
|
+
* .add(`Mode: ${colors.cyan(this.mode)}`)
|
|
10
|
+
* .add(`Ready in: ${colors.cyan(prettyHrtime(message.duration))}`)
|
|
11
|
+
*
|
|
12
|
+
* Ours says the same, `Press h to show help` included — the shortcuts are in
|
|
13
|
+
* `./shortcuts.ts`, so the line is an offer this process keeps.
|
|
14
|
+
*
|
|
15
|
+
* Printed from the application rather than from the `ream dev` parent, which
|
|
16
|
+
* is where upstream prints it. The parent is a Rust process here and the port
|
|
17
|
+
* is not its to know: a dev server that found 3000 taken binds 3001, and only
|
|
18
|
+
* the process that bound it can say so. Upstream solves the same problem by
|
|
19
|
+
* having the child send the port over IPC — the address it prints is the
|
|
20
|
+
* child's either way.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { lumen } from '@c9up/lumen'
|
|
24
|
+
|
|
25
|
+
/** Whether `ream dev` is what started this process. */
|
|
26
|
+
export function inDevServer(): boolean {
|
|
27
|
+
return process.env.REAM_DEV === 'true'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ReadyBanner {
|
|
31
|
+
/** The host the server bound, as the Ignitor resolved it. */
|
|
32
|
+
host: string
|
|
33
|
+
port: number
|
|
34
|
+
/** `HMR` when the loader is swapping modules, `watch` when it only restarts. */
|
|
35
|
+
mode: string
|
|
36
|
+
/** Milliseconds from process start to listening. */
|
|
37
|
+
bootMs?: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The address to show for a bound host and port.
|
|
42
|
+
*
|
|
43
|
+
* `localhost` rather than `0.0.0.0`: bound to every interface is not a link
|
|
44
|
+
* that opens, and printing it sends you looking in the wrong place. Shared
|
|
45
|
+
* with the `o` shortcut so the browser goes where the sticker said.
|
|
46
|
+
*/
|
|
47
|
+
export function devServerUrl(host: string, port: number): string {
|
|
48
|
+
const shown = host === '0.0.0.0' || host === '::' ? 'localhost' : host
|
|
49
|
+
return `http://${shown}:${port}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The lines of the sticker, so a test can read them without a terminal. */
|
|
53
|
+
export function readyBannerLines(banner: ReadyBanner): string[] {
|
|
54
|
+
const ui = lumen()
|
|
55
|
+
const lines = [
|
|
56
|
+
`Server address: ${ui.colors.cyan(devServerUrl(banner.host, banner.port))}`,
|
|
57
|
+
`Mode: ${ui.colors.cyan(banner.mode)}`,
|
|
58
|
+
]
|
|
59
|
+
if (banner.bootMs !== undefined) {
|
|
60
|
+
lines.push(`Ready in: ${ui.colors.cyan(formatBootTime(banner.bootMs))}`)
|
|
61
|
+
}
|
|
62
|
+
// Only where there is a keyboard: piped into a file, the offer is a lie.
|
|
63
|
+
if (process.stdin.isTTY) lines.push(`Press ${ui.colors.dim('h')} to show help`)
|
|
64
|
+
return lines
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Print it, the way upstream prints its sticker. */
|
|
68
|
+
export function printReadyBanner(banner: ReadyBanner): void {
|
|
69
|
+
const ui = lumen()
|
|
70
|
+
const sticker = ui.sticker()
|
|
71
|
+
for (const line of readyBannerLines(banner)) sticker.add(line)
|
|
72
|
+
sticker.render()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `412 ms`, `1.2 s` — upstream runs the duration through `pretty-hrtime`.
|
|
77
|
+
*
|
|
78
|
+
* Not that dependency: this prints one number once per boot, and the package
|
|
79
|
+
* is 60 lines of unit selection for a case we have exactly one of.
|
|
80
|
+
*/
|
|
81
|
+
function formatBootTime(ms: number): string {
|
|
82
|
+
if (ms < 1000) return `${Math.round(ms)} ms`
|
|
83
|
+
return `${(ms / 1000).toFixed(1)} s`
|
|
84
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keys the dev server answers to.
|
|
3
|
+
*
|
|
4
|
+
* Upstream's set, read off `@adonisjs/assembler`'s build: `r` restarts, `c`
|
|
5
|
+
* clears, `o` opens the browser, `h` shows the list, and Ctrl-C or Ctrl-D
|
|
6
|
+
* quits. Same keys, same order, same words.
|
|
7
|
+
*
|
|
8
|
+
* Owned by the application rather than by the `ream dev` parent, which is
|
|
9
|
+
* where upstream owns it — the same split as the ready sticker, and for the
|
|
10
|
+
* same reason: `o` needs the address, and a port found by scanning past a
|
|
11
|
+
* taken 3000 is known only to the process that bound it. Everything else
|
|
12
|
+
* follows from there, because this process already has what each key needs:
|
|
13
|
+
* exiting on the restart code is what makes the parent start it again, and
|
|
14
|
+
* the graceful shutdown is already wired to a signal.
|
|
15
|
+
*
|
|
16
|
+
* NO dependency for opening a browser. Upstream imports `open`; this spawns
|
|
17
|
+
* the platform's opener, which is the one thing that package does on the three
|
|
18
|
+
* platforms a dev server runs on.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { spawn } from 'node:child_process'
|
|
22
|
+
import { lumen } from '@c9up/lumen'
|
|
23
|
+
import { clearScreen } from './fullReload.js'
|
|
24
|
+
|
|
25
|
+
/** The exit code the `ream dev` parent restarts on. */
|
|
26
|
+
export const RESTART_EXIT_CODE = 75
|
|
27
|
+
|
|
28
|
+
interface Shortcut {
|
|
29
|
+
key: string
|
|
30
|
+
description: string
|
|
31
|
+
handler: () => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ShortcutsOptions {
|
|
35
|
+
/** Where the server is listening, for `o`. */
|
|
36
|
+
url?: string
|
|
37
|
+
/** Leave the process the way a restart leaves it. Defaults to exiting. */
|
|
38
|
+
onRestart?: () => void
|
|
39
|
+
/** End the session. Defaults to exiting through the normal shutdown. */
|
|
40
|
+
onQuit?: () => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The lines `h` prints, so a test can read them without a terminal. */
|
|
44
|
+
export function helpLines(): string[] {
|
|
45
|
+
return ['Available shortcuts:', ...shortcutsFor({}).map((s) => `· ${s.key}: ${s.description}`)]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shortcutsFor(options: ShortcutsOptions): Shortcut[] {
|
|
49
|
+
const ui = lumen()
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
key: 'r',
|
|
53
|
+
description: 'restart server',
|
|
54
|
+
handler: () => {
|
|
55
|
+
ui.logger.log('')
|
|
56
|
+
ui.logger.info('Manual restart triggered...')
|
|
57
|
+
// Exiting on the restart code IS the restart: the parent loops on it,
|
|
58
|
+
// which is the same path a change outside the boundaries takes.
|
|
59
|
+
if (options.onRestart) options.onRestart()
|
|
60
|
+
else process.exit(RESTART_EXIT_CODE)
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: 'c',
|
|
65
|
+
description: 'clear console',
|
|
66
|
+
handler: () => {
|
|
67
|
+
clearScreen()
|
|
68
|
+
ui.logger.info('Console cleared')
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'o',
|
|
73
|
+
description: 'open in browser',
|
|
74
|
+
handler: () => {
|
|
75
|
+
if (options.url === undefined) {
|
|
76
|
+
ui.logger.warning('No server address to open')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
ui.logger.log('')
|
|
80
|
+
ui.logger.info(`Opening ${options.url}...`)
|
|
81
|
+
openBrowser(options.url)
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: 'h',
|
|
86
|
+
description: 'show this help',
|
|
87
|
+
handler: () => {
|
|
88
|
+
ui.logger.log('')
|
|
89
|
+
for (const line of helpLines()) ui.logger.log(line)
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** What to do with one keypress. Exported so the mapping is testable. */
|
|
96
|
+
export function handleKey(key: string, options: ShortcutsOptions = {}): boolean {
|
|
97
|
+
// \u0003 is Ctrl-C and \u0004 is Ctrl-D. Raw mode means no signal is raised
|
|
98
|
+
// for either, so the process would sit there unkillable without this.
|
|
99
|
+
if (key === '\u0003' || key === '\u0004') {
|
|
100
|
+
if (options.onQuit) options.onQuit()
|
|
101
|
+
else process.kill(process.pid, 'SIGINT')
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
const shortcut = shortcutsFor(options).find((entry) => entry.key === key)
|
|
105
|
+
if (shortcut === undefined) return false
|
|
106
|
+
shortcut.handler()
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Listen for keys. Answers a function that stops listening.
|
|
112
|
+
*
|
|
113
|
+
* A no-op when stdin is not a terminal — `ream dev` piped into a file has no
|
|
114
|
+
* keyboard, and raw mode on a pipe throws.
|
|
115
|
+
*/
|
|
116
|
+
export function installShortcuts(options: ShortcutsOptions = {}): () => void {
|
|
117
|
+
if (!process.stdin.isTTY) return () => {}
|
|
118
|
+
|
|
119
|
+
const onData = (data: Buffer | string): void => {
|
|
120
|
+
handleKey(data.toString(), options)
|
|
121
|
+
}
|
|
122
|
+
process.stdin.setRawMode(true)
|
|
123
|
+
process.stdin.on('data', onData)
|
|
124
|
+
// Nothing should stay alive for a keyboard: a server that finished its work
|
|
125
|
+
// must be able to exit with a listener still attached.
|
|
126
|
+
process.stdin.unref()
|
|
127
|
+
|
|
128
|
+
return () => {
|
|
129
|
+
process.stdin.setRawMode(false)
|
|
130
|
+
process.stdin.removeListener('data', onData)
|
|
131
|
+
process.stdin.pause()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Hand the URL to whatever opens links here.
|
|
137
|
+
*
|
|
138
|
+
* Detached and ignored: an opener that writes to stderr, or outlives the dev
|
|
139
|
+
* server, must not take the terminal or the exit with it.
|
|
140
|
+
*/
|
|
141
|
+
function openBrowser(url: string): void {
|
|
142
|
+
const command =
|
|
143
|
+
process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
|
|
144
|
+
try {
|
|
145
|
+
const child = spawn(command, [url], {
|
|
146
|
+
detached: true,
|
|
147
|
+
stdio: 'ignore',
|
|
148
|
+
shell: process.platform === 'win32',
|
|
149
|
+
})
|
|
150
|
+
child.unref()
|
|
151
|
+
child.on('error', () => {
|
|
152
|
+
// No opener on this machine — a headless container, say. The address was
|
|
153
|
+
// printed a moment ago; losing the click is not worth a crash.
|
|
154
|
+
})
|
|
155
|
+
} catch {
|
|
156
|
+
// Same.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ReamError } from './ReamError.js'
|
|
10
|
+
import { renderError } from './renderError.js'
|
|
10
11
|
|
|
11
12
|
export function prettyPrintError(error: unknown): void {
|
|
13
|
+
// A framework error already says what it is, what it was doing and what to
|
|
14
|
+
// try — printing a source excerpt under it would bury that.
|
|
12
15
|
if (error instanceof ReamError) {
|
|
13
16
|
console.error(error.toDevString())
|
|
14
|
-
|
|
15
|
-
console.error(`\n ${error.message}\n`)
|
|
16
|
-
if (error.stack) console.error(error.stack)
|
|
17
|
-
} else {
|
|
18
|
-
console.error(error)
|
|
17
|
+
return
|
|
19
18
|
}
|
|
19
|
+
console.error(renderError(error))
|
|
20
20
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An error, rendered for someone reading a terminal.
|
|
3
|
+
*
|
|
4
|
+
* Upstream hands this to youch. Written here instead, for the reason lumen was
|
|
5
|
+
* written rather than a colour package taken: a dependency is code we do not
|
|
6
|
+
* control, shipped into every application that installs the framework.
|
|
7
|
+
*
|
|
8
|
+
* The expensive part of doing this properly is usually reading source maps to
|
|
9
|
+
* get from a transpiled frame back to the line someone wrote. It is not needed
|
|
10
|
+
* here: `ream dev` runs TypeScript through swc-node, which already reports
|
|
11
|
+
* `app/controllers/users_controller.ts:24:11` — measured, not assumed. A build
|
|
12
|
+
* that does not, or a frame whose file cannot be read, falls back to the plain
|
|
13
|
+
* stack rather than showing nothing.
|
|
14
|
+
*
|
|
15
|
+
* NOTHING here may throw. This is what runs when something has already gone
|
|
16
|
+
* wrong, and an error thrown while rendering an error replaces the one you
|
|
17
|
+
* needed to see.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from 'node:fs'
|
|
21
|
+
import { relative, sep } from 'node:path'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
import { ansiColors, silentColors, supportsColor } from '@c9up/lumen'
|
|
24
|
+
|
|
25
|
+
/** One line of a stack, taken apart. */
|
|
26
|
+
export interface StackFrame {
|
|
27
|
+
/** The function, when the runtime named one. */
|
|
28
|
+
callee?: string
|
|
29
|
+
file: string
|
|
30
|
+
line: number
|
|
31
|
+
column: number
|
|
32
|
+
/** Where it comes from, which is what decides how much of it is shown. */
|
|
33
|
+
origin: 'application' | 'dependency' | 'runtime'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RenderOptions {
|
|
37
|
+
/** Paths are printed relative to this. Defaults to the working directory. */
|
|
38
|
+
cwd?: string
|
|
39
|
+
/** Lines of source shown around the failing one. */
|
|
40
|
+
context?: number
|
|
41
|
+
/** Force colour on or off. Defaults to what the stream takes. */
|
|
42
|
+
colors?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const AT_WITH_CALLEE = /^\s*at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/
|
|
46
|
+
const AT_BARE = /^\s*at\s+(.+):(\d+):(\d+)$/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Take a stack apart.
|
|
50
|
+
*
|
|
51
|
+
* A line that matches neither shape is dropped rather than guessed at: a
|
|
52
|
+
* malformed frame in the middle of a trace is worse than a shorter trace.
|
|
53
|
+
*/
|
|
54
|
+
export function parseStack(stack: string | undefined, cwd: string): StackFrame[] {
|
|
55
|
+
if (stack === undefined) return []
|
|
56
|
+
const frames: StackFrame[] = []
|
|
57
|
+
|
|
58
|
+
for (const raw of stack.split('\n').slice(1)) {
|
|
59
|
+
const withCallee = AT_WITH_CALLEE.exec(raw)
|
|
60
|
+
const bare = withCallee === null ? AT_BARE.exec(raw) : null
|
|
61
|
+
const parts = withCallee ?? bare
|
|
62
|
+
if (parts === null) continue
|
|
63
|
+
|
|
64
|
+
const callee = withCallee === null ? undefined : parts[1]
|
|
65
|
+
const location = withCallee === null ? parts[1] : parts[2]
|
|
66
|
+
const line = Number(withCallee === null ? parts[2] : parts[3])
|
|
67
|
+
const column = Number(withCallee === null ? parts[3] : parts[4])
|
|
68
|
+
if (location === undefined || !Number.isInteger(line)) continue
|
|
69
|
+
|
|
70
|
+
const file = toPath(location)
|
|
71
|
+
frames.push({
|
|
72
|
+
...(callee === undefined ? {} : { callee }),
|
|
73
|
+
file,
|
|
74
|
+
line,
|
|
75
|
+
column: Number.isInteger(column) ? column : 0,
|
|
76
|
+
origin: originOf(file, cwd),
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
return frames
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The whole thing: message, the failing line in context, the trace, the cause. */
|
|
83
|
+
export function renderError(error: unknown, options: RenderOptions = {}): string {
|
|
84
|
+
try {
|
|
85
|
+
return render(error, options, new Set())
|
|
86
|
+
} catch {
|
|
87
|
+
// The renderer failed on the error it was asked to show. Whatever it was
|
|
88
|
+
// is still more useful than a second failure.
|
|
89
|
+
return String(error instanceof Error ? (error.stack ?? error.message) : error)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function render(error: unknown, options: RenderOptions, seen: Set<unknown>): string {
|
|
94
|
+
const colors = options.colors === undefined ? forStream() : palette(options.colors)
|
|
95
|
+
const cwd = options.cwd ?? process.cwd()
|
|
96
|
+
|
|
97
|
+
if (!(error instanceof Error)) {
|
|
98
|
+
return `\n ${colors.red('Error')} ${String(error)}\n`
|
|
99
|
+
}
|
|
100
|
+
// A cause that points back at something already rendered would recurse
|
|
101
|
+
// forever; errors do get wired in circles.
|
|
102
|
+
if (seen.has(error)) return ''
|
|
103
|
+
seen.add(error)
|
|
104
|
+
|
|
105
|
+
const out: string[] = ['']
|
|
106
|
+
const name = error.name === '' ? 'Error' : error.name
|
|
107
|
+
out.push(` ${colors.bgRed(colors.white(` ${name} `))} ${colors.red(error.message)}`)
|
|
108
|
+
|
|
109
|
+
const frames = parseStack(error.stack, cwd)
|
|
110
|
+
const first = frames.find((frame) => frame.origin === 'application') ?? frames[0]
|
|
111
|
+
|
|
112
|
+
if (first !== undefined) {
|
|
113
|
+
const excerpt = sourceExcerpt(first, options.context ?? 3, colors)
|
|
114
|
+
if (excerpt.length > 0) {
|
|
115
|
+
out.push('')
|
|
116
|
+
out.push(` ${colors.dim(`${relativeTo(first.file, cwd)}:${first.line}:${first.column}`)}`)
|
|
117
|
+
out.push('')
|
|
118
|
+
out.push(...excerpt)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const trace = frames.filter((frame) => frame.origin !== 'runtime')
|
|
123
|
+
if (trace.length > 0) {
|
|
124
|
+
out.push('')
|
|
125
|
+
for (const frame of trace.slice(0, 12)) {
|
|
126
|
+
const where = `${relativeTo(frame.file, cwd)}:${frame.line}`
|
|
127
|
+
const callee = frame.callee === undefined ? '' : `${frame.callee} `
|
|
128
|
+
const line = ` at ${callee}${where}`
|
|
129
|
+
// A frame from a dependency is dimmed rather than dropped: it is where
|
|
130
|
+
// the failure passed through, and hiding it makes a trace lie.
|
|
131
|
+
out.push(frame.origin === 'application' ? line : colors.dim(line))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const cause = 'cause' in error ? error.cause : undefined
|
|
136
|
+
if (cause !== undefined && cause !== null) {
|
|
137
|
+
const rendered = render(cause, options, seen)
|
|
138
|
+
if (rendered !== '') {
|
|
139
|
+
out.push('')
|
|
140
|
+
out.push(` ${colors.dim('caused by')}`)
|
|
141
|
+
out.push(rendered)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
out.push('')
|
|
146
|
+
return out.join('\n')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The failing line, with the ones around it.
|
|
151
|
+
*
|
|
152
|
+
* Empty when the file cannot be read — a frame inside a bundle, a file deleted
|
|
153
|
+
* since the process started, a permission. The trace still prints.
|
|
154
|
+
*/
|
|
155
|
+
function sourceExcerpt(
|
|
156
|
+
frame: StackFrame,
|
|
157
|
+
context: number,
|
|
158
|
+
colors: ReturnType<typeof ansiColors>,
|
|
159
|
+
): string[] {
|
|
160
|
+
let source: string
|
|
161
|
+
try {
|
|
162
|
+
source = readFileSync(frame.file, 'utf8')
|
|
163
|
+
} catch {
|
|
164
|
+
return []
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const lines = source.split('\n')
|
|
168
|
+
const start = Math.max(1, frame.line - context)
|
|
169
|
+
const end = Math.min(lines.length, frame.line + context)
|
|
170
|
+
const gutter = String(end).length
|
|
171
|
+
|
|
172
|
+
const out: string[] = []
|
|
173
|
+
for (let number = start; number <= end; number++) {
|
|
174
|
+
const text = lines[number - 1] ?? ''
|
|
175
|
+
const label = String(number).padStart(gutter, ' ')
|
|
176
|
+
if (number === frame.line) {
|
|
177
|
+
out.push(` ${colors.red('❯')} ${colors.red(label)} │ ${text}`)
|
|
178
|
+
if (frame.column > 0) {
|
|
179
|
+
// The caret under the column the runtime reported. Tabs are copied
|
|
180
|
+
// across so the caret lands under the character, not beside it.
|
|
181
|
+
const prefix = text.slice(0, frame.column - 1).replace(/[^\t]/g, ' ')
|
|
182
|
+
out.push(` ${' '.repeat(gutter)} │ ${prefix}${colors.red('^')}`)
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
out.push(` ${colors.dim(label)} ${colors.dim('│')} ${colors.dim(text)}`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** `file:///a/b.ts` and `/a/b.ts` both name the same file. */
|
|
192
|
+
function toPath(location: string): string {
|
|
193
|
+
if (!location.startsWith('file:')) return location
|
|
194
|
+
try {
|
|
195
|
+
return fileURLToPath(location)
|
|
196
|
+
} catch {
|
|
197
|
+
return location
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function originOf(file: string, cwd: string): StackFrame['origin'] {
|
|
202
|
+
if (file.startsWith('node:') || !file.includes(sep)) return 'runtime'
|
|
203
|
+
if (file.includes(`${sep}node_modules${sep}`)) return 'dependency'
|
|
204
|
+
return file.startsWith(cwd) ? 'application' : 'dependency'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The shortest path that still says where a frame is.
|
|
209
|
+
*
|
|
210
|
+
* A dependency frame is cut back to what comes after the last
|
|
211
|
+
* `node_modules/`, because the part before it is a store layout nobody reads:
|
|
212
|
+
* `@vitest/runner/dist/chunk-artifact.js`, not
|
|
213
|
+
* `…/.pnpm/@vitest+runner@4.1.11/node_modules/@vitest/runner/dist/…`.
|
|
214
|
+
*/
|
|
215
|
+
function relativeTo(file: string, cwd: string): string {
|
|
216
|
+
if (file.startsWith('node:')) return file
|
|
217
|
+
const marker = `${sep}node_modules${sep}`
|
|
218
|
+
const last = file.lastIndexOf(marker)
|
|
219
|
+
if (last !== -1) return file.slice(last + marker.length)
|
|
220
|
+
const path = relative(cwd, file)
|
|
221
|
+
return path === '' || path.startsWith('..') ? file : path
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function forStream(): ReturnType<typeof ansiColors> {
|
|
225
|
+
return supportsColor(process.stderr) ? ansiColors() : silentColors()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function palette(on: boolean): ReturnType<typeof ansiColors> {
|
|
229
|
+
return on ? ansiColors() : silentColors()
|
|
230
|
+
}
|
package/src/hot.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
28
28
|
import { registerHooks } from 'node:module'
|
|
29
29
|
import { dirname, relative, resolve as resolvePath } from 'node:path'
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
31
|
-
import { fullReloadNotice } from './dev/fullReload.js'
|
|
31
|
+
import { clearScreen, fullReloadNotice, invalidatedNotice } from './dev/fullReload.js'
|
|
32
32
|
import { hotReloadHappened } from './dev/hmr.js'
|
|
33
33
|
import { readHotHookConfig } from './dev/hotConfig.js'
|
|
34
34
|
import { globMatcher } from './dev/hotGlob.js'
|
|
@@ -184,10 +184,20 @@ watchProject({
|
|
|
184
184
|
...reloadPatterns.map(watchRootOf).filter((dir) => dir !== ''),
|
|
185
185
|
],
|
|
186
186
|
files: [...restartFiles].map((file) => relative(root, file)),
|
|
187
|
+
// A watcher that quietly stops watching is how this went unnoticed for a
|
|
188
|
+
// release: the first save swapped, the rest did nothing at all.
|
|
189
|
+
onWarning: (message) => sayNow(`[ream] hot reload — ${message}`),
|
|
187
190
|
onChange: (file) => {
|
|
188
191
|
const decision = graph.decide(file)
|
|
192
|
+
// Silent, as upstream is: a file the running process never imported has
|
|
193
|
+
// nothing to swap and nothing to restart for, and upstream's dev server
|
|
194
|
+
// returns without a word when its own inspection comes back empty.
|
|
189
195
|
if (decision.kind === 'ignore') return
|
|
190
196
|
if (decision.kind === 'reload') {
|
|
197
|
+
// Upstream clears the terminal before this line, so the logs that follow
|
|
198
|
+
// the restart are the only ones on screen. `--no-clear` turns it off,
|
|
199
|
+
// which is the flag `ace serve` gives for the same thing.
|
|
200
|
+
clearScreen()
|
|
191
201
|
sayNow(fullReloadNotice(decision.file, decision.reason, relativeToRoot))
|
|
192
202
|
// Exiting on a known code is how the Rust parent learns it must restart —
|
|
193
203
|
// see `EXIT_RESTART` in the CLI's dev module.
|
|
@@ -198,6 +208,6 @@ watchProject({
|
|
|
198
208
|
// process keeps serving the class it promoted: a stable PID and stale
|
|
199
209
|
// output, which is exactly how it was first reported.
|
|
200
210
|
hotReloadHappened()
|
|
201
|
-
sayNow(
|
|
211
|
+
sayNow(invalidatedNotice(file, relativeToRoot))
|
|
202
212
|
},
|
|
203
213
|
})
|
package/src/index.ts
CHANGED
|
@@ -249,6 +249,12 @@ export type {
|
|
|
249
249
|
CommandOptions,
|
|
250
250
|
FlagMetaData,
|
|
251
251
|
} from './console/types.js'
|
|
252
|
+
export {
|
|
253
|
+
parseStack,
|
|
254
|
+
type RenderOptions,
|
|
255
|
+
renderError,
|
|
256
|
+
type StackFrame,
|
|
257
|
+
} from './errors/renderError.js'
|
|
252
258
|
export type {
|
|
253
259
|
HyperServerLike,
|
|
254
260
|
IgnitorConfig,
|
|
@@ -272,8 +278,22 @@ export type {
|
|
|
272
278
|
|
|
273
279
|
// ─── Codemods ───────────────────────────────────────────────
|
|
274
280
|
|
|
275
|
-
export
|
|
276
|
-
|
|
281
|
+
export {
|
|
282
|
+
type Codemods,
|
|
283
|
+
createCodemods,
|
|
284
|
+
type GeneratedStub,
|
|
285
|
+
type StubState,
|
|
286
|
+
} from './Codemods.js'
|
|
287
|
+
export {
|
|
288
|
+
compile as compileStub,
|
|
289
|
+
escapeHtml,
|
|
290
|
+
generate as generateStub,
|
|
291
|
+
render as renderStub,
|
|
292
|
+
type StubBlock,
|
|
293
|
+
StubSyntaxError,
|
|
294
|
+
type StubValue,
|
|
295
|
+
type TemplateOptions,
|
|
296
|
+
} from './stubs/template.js'
|
|
277
297
|
|
|
278
298
|
// ─── Errors ─────────────────────────────────────────────────
|
|
279
299
|
|
|
@@ -385,4 +405,4 @@ export {
|
|
|
385
405
|
StatsTracker,
|
|
386
406
|
} from './scheduler/index.js'
|
|
387
407
|
|
|
388
|
-
export const VERSION = '0.2.
|
|
408
|
+
export const VERSION = '0.2.27'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where profiles live during a dev session.
|
|
3
|
+
*
|
|
4
|
+
* In memory and bounded. Symfony writes its profiles to disk so they survive a
|
|
5
|
+
* restart; here a restart is the common case — every saved file changes it —
|
|
6
|
+
* and a dev server that grows a directory nobody empties is a worse trade than
|
|
7
|
+
* losing the history of the session you just ended.
|
|
8
|
+
*
|
|
9
|
+
* The bound is a count, not a size: a profile holding 400 queries is the one
|
|
10
|
+
* you most want to keep, and dropping it for being big would lose exactly the
|
|
11
|
+
* request worth looking at.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Profile, ProfilerCollector, ProfileStore, ProfileSummary } from './types.js'
|
|
15
|
+
|
|
16
|
+
/** How many requests are remembered. Enough to walk back through a flow. */
|
|
17
|
+
const DEFAULT_LIMIT = 50
|
|
18
|
+
|
|
19
|
+
export class MemoryProfileStore implements ProfileStore {
|
|
20
|
+
readonly #profiles = new Map<string, Profile>()
|
|
21
|
+
readonly #limit: number
|
|
22
|
+
readonly #collectors: readonly ProfilerCollector[]
|
|
23
|
+
|
|
24
|
+
constructor(collectors: readonly ProfilerCollector[] = [], limit = DEFAULT_LIMIT) {
|
|
25
|
+
this.#collectors = collectors
|
|
26
|
+
this.#limit = Math.max(1, limit)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
save(profile: Profile): void {
|
|
30
|
+
this.#profiles.set(profile.token, profile)
|
|
31
|
+
// A Map iterates in insertion order, so the oldest key is the first one.
|
|
32
|
+
while (this.#profiles.size > this.#limit) {
|
|
33
|
+
const oldest = this.#profiles.keys().next()
|
|
34
|
+
if (oldest.done === true) break
|
|
35
|
+
this.#profiles.delete(oldest.value)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
find(token: string): Profile | undefined {
|
|
40
|
+
return this.#profiles.get(token)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Newest first, which is the order anyone reading a history wants. */
|
|
44
|
+
recent(limit = 20): ProfileSummary[] {
|
|
45
|
+
const summaries: ProfileSummary[] = []
|
|
46
|
+
for (const profile of [...this.#profiles.values()].reverse()) {
|
|
47
|
+
if (summaries.length >= limit) break
|
|
48
|
+
summaries.push(this.#summarise(profile))
|
|
49
|
+
}
|
|
50
|
+
return summaries
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
clear(): void {
|
|
54
|
+
this.#profiles.clear()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The headline each collector wants shown.
|
|
59
|
+
*
|
|
60
|
+
* Asked of the collector rather than computed here: what counts as the
|
|
61
|
+
* headline of a panel is the panel's business, and a store that knew would
|
|
62
|
+
* have to be changed every time one is added.
|
|
63
|
+
*/
|
|
64
|
+
#summarise(profile: Profile): ProfileSummary {
|
|
65
|
+
const badges: Record<string, string> = {}
|
|
66
|
+
for (const collector of this.#collectors) {
|
|
67
|
+
const data = profile.data[collector.name]
|
|
68
|
+
if (data === undefined || collector.badge === undefined) continue
|
|
69
|
+
const badge = collector.badge(data)
|
|
70
|
+
if (badge !== undefined) badges[collector.name] = badge
|
|
71
|
+
}
|
|
72
|
+
const { data: _data, ...rest } = profile
|
|
73
|
+
return { ...rest, badges }
|
|
74
|
+
}
|
|
75
|
+
}
|