@crab-dev/wake 0.1.2 → 0.1.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4
4
+
5
+ - Added the opt-in Storybook-like documentation component workbench.
6
+ - Fixed Yarn PnP packages that expose entries and subpaths through `package.json#exports`.
7
+ - Expanded TypeScript and TSX parsing for optional arrow parameters, generic JSX, generic async arrows, and indexed type queries.
8
+ - Added source module paths to build diagnostics and corrected `wake parse` source-type selection.
9
+
10
+ ## 0.1.3
11
+
12
+ - Improved the npm development-server terminal panel with Rust CLI-aligned styling, startup timing, rebuild progress, diagnostics, and color controls.
13
+
3
14
  ## 0.1.2
4
15
 
5
16
  - Updated the npm release pipeline for private GitHub repositories while preserving immutable tarball audits.
@@ -0,0 +1,459 @@
1
+ const RESET = '\x1b[0m'
2
+ const BOLD = '\x1b[1m'
3
+ const DIM = '\x1b[2m'
4
+ const MAX_ACTIVITY = 200
5
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧']
6
+
7
+ export function supportsColor(stream = process.stderr, env = process.env) {
8
+ return stream.isTTY === true && !Object.hasOwn(env, 'NO_COLOR')
9
+ }
10
+
11
+ export function supportsTui(input = process.stdin, output = process.stderr, env = process.env) {
12
+ return input.isTTY === true
13
+ && output.isTTY === true
14
+ && String(env.TERM || '').toLowerCase() !== 'dumb'
15
+ }
16
+
17
+ export function createUi(color = supportsColor(), env = process.env) {
18
+ const trueColor = color && /^(truecolor|24bit)$/i.test(env.COLORTERM || '')
19
+ const wrap = (indexed, rgb, text) => {
20
+ if (!color) return String(text)
21
+ const code = trueColor
22
+ ? `\x1b[38;2;${rgb.join(';')}m`
23
+ : `\x1b[38;5;${indexed}m`
24
+ return `${code}${text}${RESET}`
25
+ }
26
+ return {
27
+ color,
28
+ accent: (text) => wrap(81, [34, 211, 238], text),
29
+ bold: (text) => color ? `${BOLD}${text}${RESET}` : String(text),
30
+ brand: (text) => color ? `${BOLD}${wrap(213, [217, 70, 239], text)}${RESET}` : String(text),
31
+ dim: (text) => color ? `${DIM}${text}${RESET}` : String(text),
32
+ error: (text) => wrap(204, [251, 113, 133], text),
33
+ ok: (text) => wrap(114, [74, 222, 128], text),
34
+ warn: (text) => wrap(214, [251, 191, 36], text),
35
+ }
36
+ }
37
+
38
+ export function humanDuration(durationMs) {
39
+ const milliseconds = Math.max(1, Number(durationMs) || 0)
40
+ if (milliseconds < 1_000) return `${milliseconds.toFixed(0)}ms`
41
+ if (milliseconds < 60_000) return `${(milliseconds / 1_000).toFixed(2)}s`
42
+ const seconds = milliseconds / 1_000
43
+ return `${Math.floor(seconds / 60)}m${(seconds % 60).toFixed(1)}s`
44
+ }
45
+
46
+ function moduleCount(count) {
47
+ return `${count} module${count === 1 ? '' : 's'}`
48
+ }
49
+
50
+ function cacheHitCount(count) {
51
+ return `${count} cache hit${count === 1 ? '' : 's'}`
52
+ }
53
+
54
+ export function humanRuntime(durationMs) {
55
+ const seconds = Math.max(0, Math.floor(durationMs / 1_000))
56
+ if (seconds < 60) return `${seconds}s`
57
+ if (seconds < 3_600) return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`
58
+ return `${Math.floor(seconds / 3_600)}h${String(Math.floor((seconds % 3_600) / 60)).padStart(2, '0')}m`
59
+ }
60
+
61
+ export function humanBytes(bytes) {
62
+ const value = Number(bytes) || 0
63
+ if (value >= 1024 * 1024) return `${(value / 1024 / 1024).toFixed(2)} MB`
64
+ if (value >= 1024) return `${(value / 1024).toFixed(1)} KB`
65
+ return `${value} B`
66
+ }
67
+
68
+ export function formatBanner(ui, command, currentVersion) {
69
+ return [
70
+ '',
71
+ ` ${ui.warn('⚡')} ${ui.brand('WAKE')} ${ui.dim('/')} ${ui.bold(command.toUpperCase())} ${ui.dim(`v${currentVersion}`)}`,
72
+ '',
73
+ ]
74
+ }
75
+
76
+ export function formatBuildResult(ui, result, label = 'Built', extra = '') {
77
+ const bytes = (result.files || []).reduce((sum, file) => sum + Number(file.bytes || 0), 0)
78
+ const lines = [
79
+ ` ${ui.ok('✓')} ${ui.bold(label)} ${ui.accent(`in ${humanDuration(result.durationMs)}`)}`,
80
+ ` ${ui.accent(`${result.moduleCount} modules`)} ${ui.dim('·')} ${(result.files || []).length} ${ui.dim('files')} ${ui.dim('·')} ${ui.accent(humanBytes(bytes))}${extra}`,
81
+ ]
82
+ if (result.outputDir) lines.push(` ${ui.dim('Output')} ${ui.accent(result.outputDir)}`)
83
+ for (const diagnostic of result.diagnostics || []) {
84
+ lines.push(` ${ui.warn(String(diagnostic.severity || 'warning').toUpperCase())} ${diagnostic.message}`)
85
+ }
86
+ lines.push('')
87
+ return lines
88
+ }
89
+
90
+ export function formatServerReady(ui, url, metrics) {
91
+ const lines = [
92
+ ` ${ui.ok('✓')} ${ui.bold('Development server ready')}`,
93
+ ` ${ui.dim('Local')} ${ui.accent(url)}`,
94
+ ]
95
+ if (metrics) {
96
+ lines.push(
97
+ ` ${ui.accent(`${metrics.modules} modules`)} ${ui.dim('·')} ${metrics.chunks} ${ui.dim('chunks')} ${ui.dim('·')} ${metrics.assets} ${ui.dim('assets')}`,
98
+ )
99
+ }
100
+ lines.push(` ${ui.dim('Press Ctrl-C to stop')}`, '')
101
+ return lines
102
+ }
103
+
104
+ export function formatError(ui, error) {
105
+ const code = error?.code ? `[${error.code}]` : '[WAKE_INTERNAL]'
106
+ const lines = [
107
+ ` ${ui.error('✗')} ${ui.bold('Wake failed')} ${ui.error(code)}`,
108
+ ` ${error?.message || error}`,
109
+ ]
110
+ if (error?.path) lines.push(` ${ui.dim('Path')} ${ui.accent(error.path)}`)
111
+ for (const diagnostic of error?.diagnostics || []) {
112
+ const diagnosticCode = diagnostic.code ? `[${diagnostic.code}] ` : ''
113
+ lines.push(
114
+ ` ${ui.warn(String(diagnostic.severity || 'error').toUpperCase())} ${diagnosticCode}${diagnostic.message}`,
115
+ )
116
+ for (const note of diagnostic.notes || []) lines.push(` ${ui.dim('·')} ${note}`)
117
+ }
118
+ lines.push('')
119
+ return lines
120
+ }
121
+
122
+ export function formatFinalSummary(ui, state, reason, label = 'Server stopped') {
123
+ const lines = [
124
+ '',
125
+ ` ${ui.dim('■')} ${ui.bold(label)} ${ui.dim(`(${reason})`)}`,
126
+ ]
127
+ if (state.endpoint) {
128
+ lines.push(` ${ui.dim(state.endpointLabel)} ${ui.accent(state.endpoint)}`)
129
+ }
130
+ lines.push(
131
+ ` ${state.rebuilds} rebuilds ${ui.dim('·')} runtime ${ui.accent(humanRuntime(Date.now() - state.startedAt))}`,
132
+ '',
133
+ )
134
+ return lines
135
+ }
136
+
137
+ export function observeServer(server, ui, output = console) {
138
+ const onRebuildStart = (event) => {
139
+ const count = event.changedPaths?.length || 0
140
+ const detail = count === 1
141
+ ? 'Rebuilding after 1 file change…'
142
+ : count > 1
143
+ ? `Rebuilding after ${count} file changes…`
144
+ : 'Rebuilding…'
145
+ output.error(` ${ui.warn('↻')} ${ui.dim(detail)}`)
146
+ }
147
+ const onRebuilt = (event) => {
148
+ if (event.initial) return
149
+ output.error(
150
+ ` ${ui.ok('✓')} ${ui.bold('Updated')} ${ui.dim('·')} ${ui.accent(moduleCount(event.updatedModules))} ${ui.dim('·')} ${ui.accent(cacheHitCount(event.cachedModules))} ${ui.accent(humanDuration(event.durationMs))}`,
151
+ )
152
+ }
153
+ const onDiagnostic = (diagnostic) => {
154
+ const code = diagnostic.code ? `[${diagnostic.code}] ` : ''
155
+ output.error(` ${ui.error('✗')} ${ui.bold('Build failed')} ${code}${diagnostic.message}`)
156
+ }
157
+
158
+ server.on('rebuildStart', onRebuildStart)
159
+ server.on('rebuilt', onRebuilt)
160
+ server.on('diagnostic', onDiagnostic)
161
+ return () => {
162
+ server.off('rebuildStart', onRebuildStart)
163
+ server.off('rebuilt', onRebuilt)
164
+ server.off('diagnostic', onDiagnostic)
165
+ }
166
+ }
167
+
168
+ function pushActivity(state, level, message) {
169
+ if (state.activity.length === MAX_ACTIVITY) state.activity.shift()
170
+ state.activity.push({
171
+ elapsedMs: Date.now() - state.startedAt,
172
+ level,
173
+ message: String(message),
174
+ })
175
+ }
176
+
177
+ export function createDashboardState({
178
+ command,
179
+ root = '.',
180
+ endpointLabel = 'LOCAL',
181
+ watchLabel = 'HMR · source maps · watching',
182
+ }) {
183
+ const state = {
184
+ command,
185
+ root,
186
+ endpointLabel,
187
+ endpoint: '',
188
+ watchLabel,
189
+ status: 'starting',
190
+ metrics: undefined,
191
+ rebuilds: 0,
192
+ startedAt: Date.now(),
193
+ activity: [],
194
+ scrollFromBottom: 0,
195
+ }
196
+ pushActivity(state, 'info', 'Starting Wake…')
197
+ return state
198
+ }
199
+
200
+ export function applyDashboardEvent(state, event) {
201
+ if (event.type === 'rebuildStart') {
202
+ const count = event.changedPaths?.length || 0
203
+ state.status = 'rebuilding'
204
+ pushActivity(
205
+ state,
206
+ 'warning',
207
+ count === 1
208
+ ? 'Rebuilding after 1 file change…'
209
+ : count > 1
210
+ ? `Rebuilding after ${count} file changes…`
211
+ : 'Rebuilding…',
212
+ )
213
+ } else if (event.type === 'rebuilt') {
214
+ state.status = 'ready'
215
+ state.metrics = {
216
+ modules: event.modules,
217
+ updatedModules: event.updatedModules,
218
+ cachedModules: event.cachedModules,
219
+ chunks: event.chunks,
220
+ assets: event.assets,
221
+ durationMs: event.durationMs,
222
+ }
223
+ if (!event.initial) state.rebuilds += 1
224
+ pushActivity(
225
+ state,
226
+ 'success',
227
+ event.initial
228
+ ? `Initial build completed: ${event.modules} modules in ${humanDuration(event.durationMs)}`
229
+ : `Updated ${moduleCount(event.updatedModules)} · ${cacheHitCount(event.cachedModules)} in ${humanDuration(event.durationMs)}`,
230
+ )
231
+ } else if (event.type === 'diagnostic') {
232
+ state.status = 'error'
233
+ pushActivity(state, 'error', event.message)
234
+ } else if (event.type === 'closed') {
235
+ state.status = 'stopped'
236
+ pushActivity(state, 'info', 'Wake stopped')
237
+ }
238
+ }
239
+
240
+ export function setDashboardEndpoint(state, endpoint) {
241
+ state.endpoint = String(endpoint)
242
+ }
243
+
244
+ export function setDashboardStopping(state, reason) {
245
+ state.status = 'stopping'
246
+ pushActivity(state, 'info', `Stopping (${reason})…`)
247
+ }
248
+
249
+ export function setDashboardStopped(state) {
250
+ state.status = 'stopped'
251
+ pushActivity(state, 'info', 'Wake stopped')
252
+ }
253
+
254
+ function statusLabel(status) {
255
+ return {
256
+ starting: 'STARTING',
257
+ ready: 'READY',
258
+ rebuilding: 'REBUILDING',
259
+ error: 'ERROR',
260
+ stopping: 'STOPPING',
261
+ stopped: 'STOPPED',
262
+ }[status] || 'STARTING'
263
+ }
264
+
265
+ function statusSymbol(state) {
266
+ if (state.status === 'ready') return '✓'
267
+ if (state.status === 'error') return '✗'
268
+ if (state.status === 'stopping') return '◌'
269
+ if (state.status === 'stopped') return '■'
270
+ return SPINNER[Math.floor((Date.now() - state.startedAt) / 100) % SPINNER.length]
271
+ }
272
+
273
+ function elapsedStamp(durationMs) {
274
+ const seconds = Math.floor(durationMs / 1_000)
275
+ return `+${String(Math.floor(seconds / 60) % 100).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`
276
+ }
277
+
278
+ function charLength(text) {
279
+ return [...stripAnsi(String(text))].length
280
+ }
281
+
282
+ function truncate(text, width) {
283
+ const chars = [...String(text)]
284
+ if (chars.length <= width) return String(text)
285
+ if (width <= 1) return chars.slice(0, width).join('')
286
+ return `${chars.slice(0, width - 1).join('')}…`
287
+ }
288
+
289
+ function pad(text, width) {
290
+ const value = truncate(text, width)
291
+ return value + ' '.repeat(Math.max(0, width - charLength(value)))
292
+ }
293
+
294
+ function boxLine(text, width) {
295
+ if (width < 4) return pad(text, width)
296
+ return `│ ${pad(text, width - 4)} │`
297
+ }
298
+
299
+ function topBorder(state, width) {
300
+ const title = ` ⚡ WAKE / ${state.command.toUpperCase()} v${state.version || ''} `
301
+ if (width < 4) return '─'.repeat(width)
302
+ const clipped = truncate(title, width - 2)
303
+ return `╭${clipped}${'─'.repeat(Math.max(0, width - 2 - charLength(clipped)))}╮`
304
+ }
305
+
306
+ function separator(width, title = '') {
307
+ if (width < 4) return '─'.repeat(width)
308
+ const center = title ? ` ${title} ` : ''
309
+ return `├${center}${'─'.repeat(Math.max(0, width - 2 - charLength(center)))}┤`
310
+ }
311
+
312
+ function bottomBorder(width) {
313
+ return width < 4 ? '─'.repeat(width) : `╰${'─'.repeat(width - 2)}╯`
314
+ }
315
+
316
+ function metricsText(state) {
317
+ const metrics = state.metrics
318
+ if (!metrics) return 'BUILD waiting for metrics…'
319
+ return `BUILD ${metrics.modules} modules · ${metrics.chunks} chunks · ${metrics.assets} assets · ${humanDuration(metrics.durationMs)}`
320
+ }
321
+
322
+ function activityRows(state, available) {
323
+ const end = Math.max(0, state.activity.length - state.scrollFromBottom)
324
+ const start = Math.max(0, end - Math.max(1, available))
325
+ return state.activity.slice(start, end).map((item) => {
326
+ const symbol = { info: '·', success: '✓', warning: '↻', error: '✗' }[item.level]
327
+ return `${elapsedStamp(item.elapsedMs)} ${symbol} ${String(item.message).replaceAll('\n', ' ')}`
328
+ })
329
+ }
330
+
331
+ function plainFrame(state, width, height) {
332
+ width = Math.max(10, width || 80)
333
+ height = Math.max(6, height || 24)
334
+ const runtime = humanRuntime(Date.now() - state.startedAt)
335
+ const header = `${statusSymbol(state)} ${statusLabel(state.status)} uptime ${runtime} ${state.rebuilds} rebuilds`
336
+ const endpoint = state.endpoint || 'waiting…'
337
+ let fixed
338
+ let activityHeight
339
+
340
+ if (width < 60 || height < 14) {
341
+ fixed = [
342
+ topBorder(state, width),
343
+ boxLine(header, width),
344
+ boxLine(state.endpoint || state.watchLabel, width),
345
+ boxLine(state.activity.at(-1)?.message || 'Starting Wake…', width),
346
+ boxLine('Resize for details · q/Ctrl-C quit', width),
347
+ ]
348
+ activityHeight = 0
349
+ } else if (width < 80 || height < 20) {
350
+ fixed = [
351
+ topBorder(state, width),
352
+ boxLine(header, width),
353
+ boxLine(`${state.endpointLabel} ${endpoint}`, width),
354
+ boxLine(metricsText(state), width),
355
+ separator(width, 'ACTIVITY'),
356
+ ]
357
+ activityHeight = Math.max(1, height - fixed.length - 2)
358
+ } else {
359
+ fixed = [
360
+ topBorder(state, width),
361
+ boxLine(header, width),
362
+ separator(width),
363
+ boxLine(`${state.endpointLabel.padEnd(7)} ${endpoint}`, width),
364
+ boxLine(`ROOT ${state.root}`, width),
365
+ boxLine(`MODE ${state.watchLabel}`, width),
366
+ separator(width),
367
+ boxLine(metricsText(state), width),
368
+ separator(width, 'ACTIVITY'),
369
+ ]
370
+ activityHeight = Math.max(1, height - fixed.length - 2)
371
+ }
372
+
373
+ for (const row of activityRows(state, activityHeight)) fixed.push(boxLine(row, width))
374
+ while (activityHeight > 0 && fixed.length < height - 2) fixed.push(boxLine('', width))
375
+ if (height >= 14) fixed.push(boxLine('↑↓/PgUp/PgDn scroll · End follow · c clear · q/Ctrl-C quit', width))
376
+ fixed.push(bottomBorder(width))
377
+ return fixed.slice(0, height)
378
+ }
379
+
380
+ function colorizeLine(line, ui) {
381
+ if (!ui.color) return line
382
+ let value = line
383
+ value = value.replace('WAKE', ui.brand('WAKE'))
384
+ value = value.replace(/(✓|■)/, (match) => ui.ok(match))
385
+ value = value.replace(/(✗)/, (match) => ui.error(match))
386
+ value = value.replace(/(↻|⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|◌)/, (match) => ui.warn(match))
387
+ value = value.replace(/(https?:\/\/\S+)/, (match) => ui.accent(match))
388
+ return value
389
+ }
390
+
391
+ export function renderDashboard(state, width = 80, height = 24, ui = createUi(false)) {
392
+ return plainFrame(state, width, height).map((line) => colorizeLine(line, ui)).join('\n')
393
+ }
394
+
395
+ export function createDashboardSession(
396
+ state,
397
+ { input = process.stdin, output = process.stderr, ui = createUi() } = {},
398
+ ) {
399
+ let closed = false
400
+ let previousRaw = false
401
+ let resolveExit
402
+ const exit = new Promise((resolve) => { resolveExit = resolve })
403
+
404
+ const draw = () => {
405
+ if (closed) return
406
+ const width = output.columns || 80
407
+ const height = output.rows || 24
408
+ output.write(`\x1b[H${renderDashboard(state, width, height, ui)}\x1b[J`)
409
+ }
410
+ const requestExit = (reason) => {
411
+ if (!closed) resolveExit(reason)
412
+ }
413
+ const onData = (chunk) => {
414
+ const key = chunk.toString('utf8')
415
+ if (key === '\u0003') requestExit('SIGINT')
416
+ else if (key === 'q' || key === 'Q') requestExit('q')
417
+ else if (key === 'c' || key === 'C') {
418
+ state.activity.length = 0
419
+ state.scrollFromBottom = 0
420
+ draw()
421
+ } else if (key === '\x1b[A') state.scrollFromBottom = Math.max(0, Math.min(state.activity.length - 1, state.scrollFromBottom + 1))
422
+ else if (key === '\x1b[B') state.scrollFromBottom = Math.max(0, state.scrollFromBottom - 1)
423
+ else if (key === '\x1b[5~') state.scrollFromBottom = Math.max(0, Math.min(state.activity.length - 1, state.scrollFromBottom + 10))
424
+ else if (key === '\x1b[6~') state.scrollFromBottom = Math.max(0, state.scrollFromBottom - 10)
425
+ else if (key === '\x1b[F' || key === '\x1b[4~') state.scrollFromBottom = 0
426
+ draw()
427
+ }
428
+ const onResize = () => draw()
429
+
430
+ previousRaw = input.isRaw === true
431
+ if (typeof input.setRawMode === 'function') input.setRawMode(true)
432
+ input.resume?.()
433
+ input.on('data', onData)
434
+ output.on?.('resize', onResize)
435
+ output.write('\x1b[?1049h\x1b[?25l\x1b[2J')
436
+ const timer = setInterval(draw, 100)
437
+ timer.unref?.()
438
+ draw()
439
+
440
+ return {
441
+ draw,
442
+ exit,
443
+ requestExit,
444
+ close() {
445
+ if (closed) return
446
+ closed = true
447
+ clearInterval(timer)
448
+ input.off('data', onData)
449
+ output.off?.('resize', onResize)
450
+ if (typeof input.setRawMode === 'function') input.setRawMode(previousRaw)
451
+ if (!previousRaw) input.pause?.()
452
+ output.write('\x1b[?25h\x1b[?1049l')
453
+ },
454
+ }
455
+ }
456
+
457
+ export function stripAnsi(value) {
458
+ return String(value).replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
459
+ }
package/bin/wake.mjs CHANGED
@@ -9,17 +9,40 @@ import {
9
9
  version,
10
10
  } from '../index.mjs'
11
11
  import { parse, tokenize } from '../experimental.mjs'
12
+ import {
13
+ applyDashboardEvent,
14
+ createDashboardSession,
15
+ createDashboardState,
16
+ createUi,
17
+ formatBanner,
18
+ formatBuildResult,
19
+ formatError,
20
+ formatFinalSummary,
21
+ formatServerReady,
22
+ observeServer,
23
+ setDashboardEndpoint,
24
+ setDashboardStopped,
25
+ setDashboardStopping,
26
+ supportsColor,
27
+ supportsTui,
28
+ } from './terminal.mjs'
12
29
 
13
30
  const HELP = `Wake ${version()}
14
31
 
15
32
  Usage:
33
+ wake [--ui auto|tui|plain] [--no-color] <command>
16
34
  wake build [entry] [--outdir DIR] [--cache] [--sourcemap]
17
35
  wake dev [root] [--entry FILE] [--host HOST] [--port PORT] [--open]
18
- wake docs build [root] [--outdir DIR] [--base PATH]
19
- wake docs dev [root] [--host HOST] [--port PORT] [--open]
20
- wake parse <file>
21
- wake tokenize <file>
36
+ wake docs build [root] [--mode site|components] [--outdir DIR] [--base PATH]
37
+ wake docs dev [root] [--mode site|components] [--host HOST] [--port PORT] [--open]
38
+ wake parse <file> [--format auto|human|json]
39
+ wake tokenize <file> [--format auto|human|json]
22
40
  wake --version
41
+
42
+ Options:
43
+ --ui MODE Terminal UI mode for long-running commands (default: auto)
44
+ --no-color Disable terminal colors; also honors NO_COLOR
45
+ --format Human or JSON output for parse/tokenize (default: auto)
23
46
  `
24
47
 
25
48
  function takeOption(args, name) {
@@ -44,37 +67,161 @@ function commonOptions(args) {
44
67
  }
45
68
  }
46
69
 
47
- function printResult(result) {
48
- const files = result.files?.length || 0
49
- console.log(
50
- `wake: built ${result.moduleCount} modules and ${files} files in ${result.durationMs.toFixed(1)}ms`,
51
- )
52
- if (result.outputDir) console.log(`wake: output ${result.outputDir}`)
70
+ function printLines(lines, output = console.error) {
71
+ for (const line of lines) output(line)
72
+ }
73
+
74
+ function validateChoice(value, name, choices) {
75
+ if (!choices.includes(value)) {
76
+ throw new Error(`${name} must be one of: ${choices.join(', ')}`)
77
+ }
78
+ return value
79
+ }
80
+
81
+ function ensureStaticMode(uiMode) {
82
+ if (uiMode === 'tui') {
83
+ throw new Error('--ui tui is only available for dev and docs dev')
84
+ }
85
+ }
86
+
87
+ function resolveTui(uiMode) {
88
+ const supported = supportsTui()
89
+ if (uiMode === 'plain') return false
90
+ if (uiMode === 'tui' && !supported) {
91
+ throw new Error('--ui tui requires interactive stdin and stderr and a capable terminal')
92
+ }
93
+ return supported
53
94
  }
54
95
 
55
- async function runServer(factory, options) {
96
+ function resolveFormat(value) {
97
+ const format = validateChoice(value || 'auto', '--format', ['auto', 'human', 'json'])
98
+ return format === 'auto' ? (process.stdout.isTTY ? 'human' : 'json') : format
99
+ }
100
+
101
+ function printResult(ui, result, label, extra = '') {
102
+ printLines(formatBuildResult(ui, result, label, extra))
103
+ }
104
+
105
+ function metricsFromEvent(event) {
106
+ return {
107
+ modules: event.modules,
108
+ updatedModules: event.updatedModules,
109
+ cachedModules: event.cachedModules,
110
+ chunks: event.chunks,
111
+ assets: event.assets,
112
+ durationMs: event.durationMs,
113
+ }
114
+ }
115
+
116
+ async function runServer(factory, options, command, root, ui, uiMode) {
56
117
  const controller = new AbortController()
57
- const server = await factory({ ...options, signal: controller.signal })
58
- console.log(`wake: listening on ${server.url}`)
59
-
60
- let stopping = false
61
- const stop = async (signal) => {
62
- if (stopping) return
63
- stopping = true
64
- controller.abort()
118
+ const useTui = resolveTui(uiMode)
119
+ const state = createDashboardState({
120
+ command,
121
+ root: root || '.',
122
+ watchLabel: command === 'docs dev'
123
+ ? 'MDX · HMR · search index · watching'
124
+ : command === 'docs components'
125
+ ? 'Demo · Controls · HMR · watching'
126
+ : 'HMR · source maps · watching',
127
+ })
128
+ state.version = version()
129
+ let dashboard
130
+ let server
131
+ let stopObserving = () => {}
132
+ let initialMetrics
133
+ let finalReason = 'server closed'
134
+
135
+ if (useTui) dashboard = createDashboardSession(state, { ui })
136
+ else printLines(formatBanner(ui, command, version()))
137
+
138
+ try {
139
+ server = await factory({ ...options, signal: controller.signal })
140
+ setDashboardEndpoint(state, server.url)
141
+
142
+ const onRebuildStart = (event) => {
143
+ applyDashboardEvent(state, event)
144
+ dashboard?.draw()
145
+ }
146
+ const onRebuilt = (event) => {
147
+ if (event.initial) initialMetrics = metricsFromEvent(event)
148
+ applyDashboardEvent(state, event)
149
+ dashboard?.draw()
150
+ }
151
+ const onDiagnostic = (diagnostic) => {
152
+ applyDashboardEvent(state, { type: 'diagnostic', message: diagnostic.message })
153
+ dashboard?.draw()
154
+ }
155
+ const onClosed = () => {
156
+ applyDashboardEvent(state, { type: 'closed' })
157
+ dashboard?.draw()
158
+ }
159
+ server.on('rebuildStart', onRebuildStart)
160
+ server.on('rebuilt', onRebuilt)
161
+ server.on('diagnostic', onDiagnostic)
162
+ server.on('closed', onClosed)
163
+
164
+ if (!useTui) stopObserving = observeServer(server, ui)
165
+ await new Promise((resolve) => setTimeout(resolve, 35))
166
+ if (!useTui) {
167
+ printLines(formatServerReady(ui, server.url, initialMetrics))
168
+ } else {
169
+ dashboard.draw()
170
+ }
171
+
172
+ let resolveSignal
173
+ const signalExit = new Promise((resolve) => { resolveSignal = resolve })
174
+ const onSigint = () => resolveSignal('SIGINT')
175
+ const onSigterm = () => resolveSignal('SIGTERM')
176
+ process.once('SIGINT', onSigint)
177
+ process.once('SIGTERM', onSigterm)
178
+
65
179
  try {
66
- await server.close()
180
+ const closed = server.waitUntilClosed().then(() => 'closed')
181
+ const reason = await Promise.race([
182
+ closed,
183
+ signalExit,
184
+ dashboard ? dashboard.exit : new Promise(() => {}),
185
+ ])
186
+ if (reason !== 'closed') {
187
+ finalReason = reason
188
+ setDashboardStopping(state, reason === 'q' ? 'q' : reason)
189
+ dashboard?.draw()
190
+ controller.abort()
191
+ await server.close()
192
+ await closed
193
+ }
194
+ setDashboardStopped(state)
195
+ dashboard?.draw()
196
+ return reason
67
197
  } finally {
68
- process.exitCode = signal === 'SIGINT' ? 130 : 143
198
+ process.off('SIGINT', onSigint)
199
+ process.off('SIGTERM', onSigterm)
200
+ server.off('rebuildStart', onRebuildStart)
201
+ server.off('rebuilt', onRebuilt)
202
+ server.off('diagnostic', onDiagnostic)
203
+ server.off('closed', onClosed)
204
+ }
205
+ } catch (error) {
206
+ if (dashboard) {
207
+ dashboard.close()
208
+ dashboard = undefined
209
+ printLines(formatBanner(ui, command, version()))
69
210
  }
211
+ throw error
212
+ } finally {
213
+ stopObserving()
214
+ dashboard?.close()
215
+ if (server) printLines(formatFinalSummary(ui, state, finalReason))
70
216
  }
71
- process.once('SIGINT', () => void stop('SIGINT'))
72
- process.once('SIGTERM', () => void stop('SIGTERM'))
73
- await server.waitUntilClosed()
74
217
  }
75
218
 
76
219
  export async function runCli(argv = process.argv.slice(2)) {
77
220
  const args = [...argv]
221
+ const noColor = takeFlag(args, '--no-color')
222
+ const uiMode = validateChoice(takeOption(args, '--ui') || 'auto', '--ui', ['auto', 'tui', 'plain'])
223
+ const ui = createUi(!noColor && supportsColor())
224
+
78
225
  if (takeFlag(args, '--version') || takeFlag(args, '-V')) {
79
226
  console.log(version())
80
227
  return 0
@@ -86,13 +233,15 @@ export async function runCli(argv = process.argv.slice(2)) {
86
233
 
87
234
  const command = args.shift()
88
235
  if (command === 'build') {
236
+ ensureStaticMode(uiMode)
89
237
  const options = commonOptions(args)
90
238
  options.outdir = takeOption(args, '--outdir')
91
239
  options.cache = takeFlag(args, '--cache')
92
240
  options.sourceMap = takeFlag(args, '--sourcemap')
93
241
  if (args[0]) options.entry = args.shift()
94
242
  if (args.length) throw new Error(`unknown build arguments: ${args.join(' ')}`)
95
- printResult(await build(options))
243
+ printLines(formatBanner(ui, 'build', version()))
244
+ printResult(ui, await build(options), 'Built')
96
245
  return 0
97
246
  }
98
247
 
@@ -105,8 +254,10 @@ export async function runCli(argv = process.argv.slice(2)) {
105
254
  options.open = takeFlag(args, '--open')
106
255
  if (args[0]) options.cwd = args.shift()
107
256
  if (args.length) throw new Error(`unknown dev arguments: ${args.join(' ')}`)
108
- await runServer(startDevServer, options)
109
- return process.exitCode || 0
257
+ const reason = await runServer(startDevServer, options, 'dev', options.cwd, ui, uiMode)
258
+ if (reason === 'SIGINT') return 130
259
+ if (reason === 'SIGTERM') return 143
260
+ return 0
110
261
  }
111
262
 
112
263
  if (command === 'docs') {
@@ -114,6 +265,7 @@ export async function runCli(argv = process.argv.slice(2)) {
114
265
  const options = commonOptions(args)
115
266
  options.outdir = takeOption(args, '--outdir')
116
267
  options.basePath = takeOption(args, '--base')
268
+ options.mode = validateChoice(takeOption(args, '--mode') || 'site', '--mode', ['site', 'components'])
117
269
  options.host = takeOption(args, '--host')
118
270
  const port = takeOption(args, '--port')
119
271
  if (port) options.port = Number(port)
@@ -121,33 +273,64 @@ export async function runCli(argv = process.argv.slice(2)) {
121
273
  if (args[0]) options.cwd = args.shift()
122
274
  if (args.length) throw new Error(`unknown docs arguments: ${args.join(' ')}`)
123
275
  if (action === 'build') {
124
- printResult(await buildDocs(options))
276
+ ensureStaticMode(uiMode)
277
+ const components = options.mode === 'components'
278
+ printLines(formatBanner(ui, components ? 'docs components build' : 'docs build', version()))
279
+ const result = await buildDocs(options)
280
+ const count = components ? (result.demos || []).length : (result.routes || []).length
281
+ const noun = components ? 'demos' : 'routes'
282
+ printResult(ui, result, components ? 'Component workbench built' : 'Documentation built', ` ${ui.dim('·')} ${count} ${noun}`)
125
283
  return 0
126
284
  }
127
285
  if (action === 'dev') {
128
- await runServer(startDocsDevServer, options)
129
- return process.exitCode || 0
286
+ const commandName = options.mode === 'components' ? 'docs components' : 'docs dev'
287
+ const reason = await runServer(startDocsDevServer, options, commandName, options.cwd, ui, uiMode)
288
+ if (reason === 'SIGINT') return 130
289
+ if (reason === 'SIGTERM') return 143
290
+ return 0
130
291
  }
131
292
  throw new Error('docs requires build or dev')
132
293
  }
133
294
 
134
295
  if (command === 'parse' || command === 'tokenize') {
296
+ ensureStaticMode(uiMode)
297
+ const format = resolveFormat(takeOption(args, '--format'))
135
298
  const file = args.shift()
136
299
  if (!file || args.length) throw new Error(`${command} requires one source file`)
137
300
  const source = await readFile(file, 'utf8')
301
+ if (format === 'human') printLines(formatBanner(ui, command, version()))
302
+
138
303
  if (command === 'tokenize') {
139
- console.log(JSON.stringify(tokenize(source), null, 2))
140
- } else {
141
- const module = parse(source, {
142
- sourceType: file.endsWith('.cjs') ? 'script' : 'module',
143
- })
144
- try {
304
+ const result = tokenize(source)
305
+ if (format === 'json') {
306
+ console.log(JSON.stringify(result, null, 2))
307
+ } else {
308
+ console.log(' START..END KIND TEXT')
309
+ for (const token of result.tokens) {
310
+ const newline = token.newlineBefore ? ' ↵' : ''
311
+ console.log(
312
+ ` ${String(token.start).padStart(5)}..${String(token.end).padEnd(5)} ${String(token.kind).padEnd(18)} ${JSON.stringify(token.text)}${newline}`,
313
+ )
314
+ }
315
+ }
316
+ return result.diagnostics?.some((diagnostic) => diagnostic.severity === 'error') ? 1 : 0
317
+ }
318
+
319
+ const module = parse(source, {
320
+ sourceType: file.endsWith('.cjs') ? 'script' : 'module',
321
+ })
322
+ try {
323
+ if (format === 'json') {
145
324
  console.log(JSON.stringify(module.summary, null, 2))
146
- } finally {
147
- module.dispose()
325
+ } else {
326
+ console.log(`Parsed ${file}`)
327
+ console.log(` Statements ${String(module.summary.statementCount).padEnd(8)} Dependencies ${module.summary.dependencies}`)
328
+ console.log(` Source bytes ${module.summary.sourceBytes}`)
148
329
  }
330
+ return module.summary.diagnostics?.some((diagnostic) => diagnostic.severity === 'error') ? 1 : 0
331
+ } finally {
332
+ module.dispose()
149
333
  }
150
- return 0
151
334
  }
152
335
 
153
336
  throw new Error(`unknown command: ${command}`)
@@ -156,10 +339,8 @@ export async function runCli(argv = process.argv.slice(2)) {
156
339
  try {
157
340
  process.exitCode = await runCli()
158
341
  } catch (error) {
159
- const code = error?.code ? `[${error.code}] ` : ''
160
- console.error(`wake: ${code}${error?.message || error}`)
161
- for (const diagnostic of error?.diagnostics || []) {
162
- console.error(` ${diagnostic.severity}: ${diagnostic.message}`)
163
- }
342
+ const noColor = process.argv.includes('--no-color')
343
+ const ui = createUi(!noColor && supportsColor())
344
+ printLines(formatError(ui, error))
164
345
  process.exitCode = 1
165
346
  }
package/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface Diagnostic {
13
13
  severity: 'error' | 'warning' | 'note' | 'help'
14
14
  code?: string
15
15
  message: string
16
+ path?: string
16
17
  start?: number
17
18
  end?: number
18
19
  notes?: string[]
@@ -47,6 +48,8 @@ export interface OutputFile {
47
48
  export interface BuildResult {
48
49
  success: true
49
50
  moduleCount: number
51
+ updatedModuleCount: number
52
+ cachedModuleCount: number
50
53
  durationMs: number
51
54
  outputDir?: string
52
55
  code?: string
@@ -59,6 +62,8 @@ export interface BundleResult extends BuildResult {
59
62
  outputDir?: undefined
60
63
  }
61
64
 
65
+ export type DocsMode = 'site' | 'components'
66
+
62
67
  export interface DocsRoute {
63
68
  id: string
64
69
  file: string
@@ -72,14 +77,26 @@ export interface DocsRoute {
72
77
  draft: boolean
73
78
  headings: Array<{ depth: number; title: string; id: string }>
74
79
  }
80
+ export interface DocsDemo {
81
+ id: string
82
+ title: string
83
+ group: string
84
+ component: string
85
+ order: number
86
+ controlCount: number
87
+ warnings: string[]
88
+ }
75
89
 
76
90
  export interface DocsBuildOptions extends ProjectOptions {
77
91
  outdir?: string
78
92
  basePath?: string
93
+ mode?: DocsMode
79
94
  }
80
95
 
81
96
  export interface DocsBuildResult extends BuildResult {
82
97
  routes: DocsRoute[]
98
+ mode: DocsMode
99
+ demos: DocsDemo[]
83
100
  }
84
101
 
85
102
  export interface DevServerOptions extends ProjectOptions {
@@ -88,6 +105,9 @@ export interface DevServerOptions extends ProjectOptions {
88
105
  port?: number
89
106
  open?: boolean
90
107
  }
108
+ export interface DocsDevServerOptions extends DevServerOptions {
109
+ mode?: DocsMode
110
+ }
91
111
 
92
112
  export interface DevServerRebuildStartEvent {
93
113
  type: 'rebuildStart'
@@ -96,7 +116,12 @@ export interface DevServerRebuildStartEvent {
96
116
 
97
117
  export interface DevServerRebuiltEvent {
98
118
  type: 'rebuilt'
119
+ initial: boolean
99
120
  modules: number
121
+ updatedModules: number
122
+ cachedModules: number
123
+ chunks: number
124
+ assets: number
100
125
  durationMs: number
101
126
  }
102
127
 
@@ -126,7 +151,7 @@ export function build(options?: BuildOptions): Promise<BuildResult>
126
151
  export function createBuildContext(options?: BuildOptions): Promise<BuildContext>
127
152
  export function startDevServer(options?: DevServerOptions): Promise<DevServer>
128
153
  export function buildDocs(options?: DocsBuildOptions): Promise<DocsBuildResult>
129
- export function startDocsDevServer(options?: DevServerOptions): Promise<DevServer>
154
+ export function startDocsDevServer(options?: DocsDevServerOptions): Promise<DevServer>
130
155
 
131
156
  declare const wake: {
132
157
  BuildContext: typeof BuildContext
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crab-dev/wake",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Wake native web build tools for Node.js",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -45,11 +45,11 @@
45
45
  "node": ">=22.14 <27"
46
46
  },
47
47
  "optionalDependencies": {
48
- "@crab-dev/wake-win32-x64-msvc": "0.1.2",
49
- "@crab-dev/wake-linux-x64-gnu": "0.1.2",
50
- "@crab-dev/wake-linux-arm64-gnu": "0.1.2",
51
- "@crab-dev/wake-darwin-x64": "0.1.2",
52
- "@crab-dev/wake-darwin-arm64": "0.1.2"
48
+ "@crab-dev/wake-darwin-arm64": "0.1.4",
49
+ "@crab-dev/wake-darwin-x64": "0.1.4",
50
+ "@crab-dev/wake-linux-arm64-gnu": "0.1.4",
51
+ "@crab-dev/wake-linux-x64-gnu": "0.1.4",
52
+ "@crab-dev/wake-win32-x64-msvc": "0.1.4"
53
53
  },
54
54
  "publishConfig": {
55
55
  "access": "public",