@ossy/app 1.10.1 → 1.11.1

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.
@@ -0,0 +1,343 @@
1
+ import util from 'node:util'
2
+
3
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
4
+
5
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`
6
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`
7
+ const green = (s) => `\x1b[32m${s}\x1b[0m`
8
+ const red = (s) => `\x1b[31m${s}\x1b[0m`
9
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`
10
+
11
+ function stripAnsi (s) {
12
+ return String(s).replace(/\x1b\[[0-9;]*m/g, '')
13
+ }
14
+
15
+ function padVisible (s, width) {
16
+ const n = stripAnsi(s).length
17
+ return n >= width ? s : s + ' '.repeat(width - n)
18
+ }
19
+
20
+ function truncateVisible (s, maxW) {
21
+ const plain = stripAnsi(s)
22
+ if (plain.length <= maxW) return s
23
+ const cut = Math.max(0, maxW - 1)
24
+ return plain.slice(0, cut) + '…'
25
+ }
26
+
27
+ export function pageIdFromHydrateEntryName (entryName) {
28
+ const p = String(entryName)
29
+ return p.startsWith('hydrate__') ? p.slice('hydrate__'.length) : p
30
+ }
31
+
32
+ function fmtPart (part, shortLabel, frame) {
33
+ if (part.s === 'idle') return dim('—')
34
+ if (part.s === 'running') {
35
+ const sp = SPINNER[frame % SPINNER.length]
36
+ return `${yellow(sp)} ${dim(shortLabel + '…')}`
37
+ }
38
+ if (part.s === 'ok') return `${green('✔')} ${dim(`${part.ms}ms`)}`
39
+ if (part.s === 'fail') return `${red('✖')} ${dim(shortLabel)}`
40
+ if (part.s === 'skip') return dim(part.note ? `— ${part.note}` : '—')
41
+ return dim('—')
42
+ }
43
+
44
+ function rowLeadIcon (r, mode) {
45
+ if (mode === 'prerender-only') {
46
+ const p = r.prerender.s
47
+ if (p === 'fail') return red('✖')
48
+ if (p === 'running') return yellow('◐')
49
+ if (p === 'ok' || p === 'skip') return green('✔')
50
+ if (p === 'idle') return dim('·')
51
+ return green('✔')
52
+ }
53
+ const b = r.bundle.s
54
+ const p = r.prerender.s
55
+ if (b === 'fail' || p === 'fail') return red('✖')
56
+ if (b === 'running' || p === 'running') return yellow('◐')
57
+ if (b === 'ok' && p === 'idle') return yellow('◐')
58
+ if (b === 'ok' && (p === 'ok' || p === 'skip')) return green('✔')
59
+ if (b === 'idle' && p === 'idle') return dim('·')
60
+ return green('✔')
61
+ }
62
+
63
+ function useTty (stream) {
64
+ return (
65
+ stream.isTTY === true &&
66
+ process.env.TERM !== 'dumb' &&
67
+ process.env.CI !== 'true' &&
68
+ process.env.OSSY_PLAIN_OUTPUT !== '1'
69
+ )
70
+ }
71
+
72
+ /**
73
+ * Merged overview + per-page progress, split layout: left = status table, right = log tail.
74
+ * TTY: single redraw block + optional console capture into the right column.
75
+ */
76
+ export function createBuildDashboard ({
77
+ scope = '@ossy/app',
78
+ mode = 'full',
79
+ pageIds,
80
+ /** @type {Record<string, string>} route path per page id */
81
+ idToPath = {},
82
+ overview = { title: '@ossy/app', configRel: null, apiRoutes: [] },
83
+ stream = process.stdout,
84
+ } = {}) {
85
+ const ids = [...pageIds].sort()
86
+ const maxId = Math.max(6, ...ids.map((id) => String(id).length))
87
+ const tty = useTty(stream)
88
+ const termW = Math.max(60, stream.columns || 100)
89
+ const LEFT_W = Math.min(56, Math.max(38, Math.floor(termW * 0.42)))
90
+ const GAP = 2
91
+ const RIGHT_W = Math.max(16, termW - LEFT_W - GAP)
92
+
93
+ /** @type {Map<string, { bundle: object, prerender: object }>} */
94
+ const rows = new Map()
95
+ for (const id of ids) {
96
+ rows.set(id, {
97
+ bundle: { s: 'idle' },
98
+ prerender: { s: 'idle' },
99
+ })
100
+ }
101
+
102
+ const logBuffer = []
103
+ const MAX_LOG = 120
104
+ let frame = 0
105
+ let spinTimer = null
106
+ let blockLines = 0
107
+ let captureActive = false
108
+ let savedConsole = null
109
+
110
+ const anyRunning = () => {
111
+ for (const r of rows.values()) {
112
+ if (r.bundle.s === 'running' || r.prerender.s === 'running') return true
113
+ }
114
+ return false
115
+ }
116
+
117
+ function pushLog (line) {
118
+ const s = String(line).trim() ? String(line) : ''
119
+ if (!s) return
120
+ if (!tty) {
121
+ stream.write(`${s}\n`)
122
+ return
123
+ }
124
+ logBuffer.push(dim(s))
125
+ if (logBuffer.length > MAX_LOG) logBuffer.splice(0, logBuffer.length - MAX_LOG)
126
+ redraw()
127
+ }
128
+
129
+ function leftHeaderLines () {
130
+ const out = []
131
+ out.push(bold(overview.title || `${scope} build`))
132
+ if (overview.configRel) {
133
+ out.push(`${dim('config')} ${truncateVisible(overview.configRel, LEFT_W - 10)}`)
134
+ }
135
+ const api = overview.apiRoutes || []
136
+ if (api.length === 0) {
137
+ /* noop */
138
+ } else if (api.length <= 2) {
139
+ for (const r of api) {
140
+ const line = `${dim('api')} ${truncateVisible(`${r.id} ${r.path}`, LEFT_W - 6)}`
141
+ out.push(line)
142
+ }
143
+ } else {
144
+ out.push(`${dim('api')} ${api.length} routes`)
145
+ }
146
+ out.push(dim('─'.repeat(Math.min(LEFT_W - 2, 44))))
147
+ return out
148
+ }
149
+
150
+ function lineForPageRow (id) {
151
+ const r = rows.get(id)
152
+ const lead = rowLeadIcon(r, mode)
153
+ const pathStr = idToPath[id] != null ? dim(String(idToPath[id])) : ''
154
+ const idCol = padVisible(dim(String(id)), maxId)
155
+ const pathPad = padVisible(pathStr, 14)
156
+ const p = fmtPart(r.prerender, 'html', frame)
157
+ if (mode === 'prerender-only') {
158
+ return ` ${lead} ${dim(scope)} ${idCol} ${pathPad} ${p}`
159
+ }
160
+ const b = fmtPart(r.bundle, 'hydrate', frame)
161
+ return ` ${lead} ${dim(scope)} ${idCol} ${pathPad} ${padVisible(b, 20)} ${p}`
162
+ }
163
+
164
+ function buildLeftColumnLines () {
165
+ const head = leftHeaderLines()
166
+ const body = ids.map((id) => lineForPageRow(id))
167
+ return [...head, ...body]
168
+ }
169
+
170
+ function buildHeaderOnlyLines () {
171
+ return leftHeaderLines()
172
+ }
173
+
174
+ /** Right column row i: logs bottom-aligned to the block height */
175
+ function rightColumnLine (i, totalRows) {
176
+ const start = logBuffer.length - totalRows + i
177
+ if (start < 0 || start >= logBuffer.length) return dim('·')
178
+ return truncateVisible(logBuffer[start], RIGHT_W)
179
+ }
180
+
181
+ function paintRow (leftLine, rightLine) {
182
+ const L = padVisible(truncateVisible(leftLine, LEFT_W), LEFT_W)
183
+ const R = truncateVisible(rightLine, RIGHT_W)
184
+ return `${L}${' '.repeat(GAP)}${R}\n`
185
+ }
186
+
187
+ function redraw () {
188
+ if (!tty) return
189
+ const leftCol = buildLeftColumnLines()
190
+ const n = leftCol.length
191
+ const lines = []
192
+ for (let i = 0; i < n; i++) {
193
+ lines.push(paintRow(leftCol[i], rightColumnLine(i, n)))
194
+ }
195
+ if (blockLines === 0) {
196
+ for (const ln of lines) stream.write(ln)
197
+ blockLines = lines.length
198
+ return
199
+ }
200
+ stream.write(`\x1b[${blockLines}A`)
201
+ for (const ln of lines) {
202
+ stream.write(`\x1b[2K\r${ln}`)
203
+ }
204
+ }
205
+
206
+ function ensureSpin () {
207
+ if (!tty || spinTimer) return
208
+ spinTimer = setInterval(() => {
209
+ frame += 1
210
+ redraw()
211
+ }, 80)
212
+ }
213
+
214
+ function stopSpin () {
215
+ if (spinTimer) {
216
+ clearInterval(spinTimer)
217
+ spinTimer = null
218
+ }
219
+ }
220
+
221
+ function nonTtyLine (id, phase, ok, ms, err) {
222
+ const label = phase === 'bundle' ? 'hydrate' : 'prerender'
223
+ const icon = ok ? green('✔') : red('✖')
224
+ const time = ms != null ? dim(` (${ms}ms)`) : ''
225
+ stream.write(`${icon} ${dim(`${scope}:${label}:`)}${id}${time}\n`)
226
+ if (err && !ok) stream.write(` ${red(String(err.message || err))}\n`)
227
+ }
228
+
229
+ function beginCapture () {
230
+ if (!tty || captureActive) return
231
+ captureActive = true
232
+ savedConsole = {
233
+ log: console.log,
234
+ warn: console.warn,
235
+ info: console.info,
236
+ error: console.error,
237
+ }
238
+ const fmt = (...a) => util.format(...a)
239
+ console.log = (...a) => {
240
+ pushLog(`[log] ${fmt(...a)}`)
241
+ }
242
+ console.warn = (...a) => {
243
+ pushLog(`[warn] ${fmt(...a)}`)
244
+ }
245
+ console.info = (...a) => {
246
+ pushLog(`[info] ${fmt(...a)}`)
247
+ }
248
+ console.error = (...a) => {
249
+ const msg = fmt(...a)
250
+ pushLog(`[error] ${msg}`)
251
+ savedConsole.error(`[@ossy/app] ${msg}`)
252
+ }
253
+ }
254
+
255
+ function endCapture () {
256
+ if (!captureActive || !savedConsole) return
257
+ console.log = savedConsole.log
258
+ console.warn = savedConsole.warn
259
+ console.info = savedConsole.info
260
+ console.error = savedConsole.error
261
+ savedConsole = null
262
+ captureActive = false
263
+ }
264
+
265
+ return {
266
+ pushLog,
267
+
268
+ /** Start optional console hijack + first paint (TTY). Plain header only when not a TTY. */
269
+ start () {
270
+ if (!tty) {
271
+ for (const ln of buildHeaderOnlyLines()) {
272
+ stream.write(`${ln}\n`)
273
+ }
274
+ stream.write('\n')
275
+ return
276
+ }
277
+ beginCapture()
278
+ redraw()
279
+ },
280
+
281
+ startBundle (pageId) {
282
+ if (mode === 'prerender-only') return
283
+ const r = rows.get(pageId)
284
+ if (!r) return
285
+ r.bundle = { s: 'running' }
286
+ ensureSpin()
287
+ redraw()
288
+ },
289
+
290
+ completeBundle (pageId, { ok, ms, error }) {
291
+ if (mode === 'prerender-only') return
292
+ const r = rows.get(pageId)
293
+ if (!r) return
294
+ r.bundle = ok ? { s: 'ok', ms } : { s: 'fail', ms, error }
295
+ if (!ok) {
296
+ r.prerender = { s: 'skip', note: 'bundle failed' }
297
+ if (!tty) nonTtyLine(pageId, 'bundle', false, ms, error)
298
+ }
299
+ if (!anyRunning()) stopSpin()
300
+ redraw()
301
+ },
302
+
303
+ startPrerender (pageId) {
304
+ const r = rows.get(pageId)
305
+ if (!r || r.prerender.s === 'skip') return
306
+ r.prerender = { s: 'running' }
307
+ ensureSpin()
308
+ redraw()
309
+ },
310
+
311
+ completePrerender (pageId, { ok, ms, error }) {
312
+ const r = rows.get(pageId)
313
+ if (!r || r.prerender.s === 'skip') return
314
+ r.prerender = ok ? { s: 'ok', ms } : { s: 'fail', ms, error }
315
+ if (!tty) nonTtyLine(pageId, 'prerender', ok, ms, error)
316
+ if (!anyRunning()) stopSpin()
317
+ redraw()
318
+ },
319
+
320
+ skipPrerender (pageId, note) {
321
+ const r = rows.get(pageId)
322
+ if (!r) return
323
+ if (r.bundle.s === 'fail') return
324
+ r.prerender = { s: 'skip', note: note || 'skipped' }
325
+ if (!tty) stream.write(`${dim('—')} ${scope}:prerender:${pageId} (${note})\n`)
326
+ redraw()
327
+ },
328
+
329
+ printSectionTitle () {
330
+ /* merged into overview.title — no-op for API compat */
331
+ },
332
+
333
+ dispose () {
334
+ stopSpin()
335
+ endCapture()
336
+ if (tty && blockLines > 0) {
337
+ redraw()
338
+ stream.write('\n')
339
+ blockLines = 0
340
+ }
341
+ },
342
+ }
343
+ }
package/cli/build.js CHANGED
@@ -12,6 +12,7 @@ import copy from 'rollup-plugin-copy';
12
12
  import replace from '@rollup/plugin-replace';
13
13
  import arg from 'arg'
14
14
  import prerenderReactTask from './prerender-react.task.js'
15
+ import { createBuildDashboard } from './build-terminal.js'
15
16
 
16
17
  export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
17
18
  export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
@@ -209,10 +210,17 @@ export function createOssyClientRollupPlugins ({ nodeEnv, copyPublicFrom, buildP
209
210
  }
210
211
 
211
212
  /** Bundles a single Node ESM file (inline dynamic imports) for SSR / API / tasks. */
212
- export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv }) {
213
+ export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv, onWarn }) {
213
214
  const bundle = await rollup({
214
215
  input: inputPath,
215
216
  plugins: createOssyAppBundlePlugins({ nodeEnv }),
217
+ onwarn (warning, defaultHandler) {
218
+ if (onWarn) {
219
+ onWarn(warning)
220
+ return
221
+ }
222
+ defaultHandler(warning)
223
+ },
216
224
  })
217
225
  await bundle.write({
218
226
  file: outputFile,
@@ -544,24 +552,46 @@ export function parsePagesFromSource(filePath) {
544
552
  }
545
553
  }
546
554
 
547
- export function printBuildOverview({
555
+ /**
556
+ * Same facts as the old build overview printout, for dashboards / plain logging.
557
+ */
558
+ export function getBuildOverviewSnapshot ({
548
559
  pagesSourcePath,
549
560
  apiSourcePath,
550
561
  apiOverviewFiles = [],
551
562
  configPath,
552
563
  pageFiles,
553
564
  }) {
554
- const rel = (p) => p ? path.relative(process.cwd(), p) : undefined
565
+ const rel = (p) => (p ? path.relative(process.cwd(), p) : undefined)
555
566
  const srcDir = path.resolve(process.cwd(), 'src')
556
- console.log('\n \x1b[1mBuild overview\x1b[0m')
557
- if (fs.existsSync(configPath)) {
558
- console.log(` \x1b[36mConfig:\x1b[0m ${rel(configPath)}`)
559
- }
560
- console.log(' ' + '─'.repeat(50))
567
+ const configRel = fs.existsSync(configPath) ? rel(configPath) : null
561
568
 
562
569
  const pages = pageFiles?.length
563
570
  ? pageFiles.map((f) => filePathToRoute(f, srcDir))
564
571
  : parsePagesFromSource(pagesSourcePath)
572
+
573
+ const apiFilesToScan =
574
+ apiOverviewFiles?.length > 0
575
+ ? apiOverviewFiles
576
+ : fs.existsSync(apiSourcePath)
577
+ ? [apiSourcePath]
578
+ : []
579
+ const apiRoutes = []
580
+ for (const f of apiFilesToScan) {
581
+ if (fs.existsSync(f)) apiRoutes.push(...parsePagesFromSource(f))
582
+ }
583
+
584
+ return { configRel, pages, apiRoutes }
585
+ }
586
+
587
+ export function printBuildOverview (opts) {
588
+ const { configRel, pages, apiRoutes } = getBuildOverviewSnapshot(opts)
589
+ console.log('\n \x1b[1mBuild overview\x1b[0m')
590
+ if (configRel) {
591
+ console.log(` \x1b[36mConfig:\x1b[0m ${configRel}`)
592
+ }
593
+ console.log(' ' + '─'.repeat(50))
594
+
565
595
  if (pages.length > 0) {
566
596
  console.log(' \x1b[36mRoutes:\x1b[0m')
567
597
  const maxId = Math.max(6, ...pages.map((p) => String(p.id).length))
@@ -575,16 +605,6 @@ export function printBuildOverview({
575
605
  console.log(' \x1b[33mRoutes:\x1b[0m (could not parse or empty)')
576
606
  }
577
607
 
578
- const apiFilesToScan =
579
- apiOverviewFiles?.length > 0
580
- ? apiOverviewFiles
581
- : fs.existsSync(apiSourcePath)
582
- ? [apiSourcePath]
583
- : []
584
- const apiRoutes = []
585
- for (const f of apiFilesToScan) {
586
- if (fs.existsSync(f)) apiRoutes.push(...parsePagesFromSource(f))
587
- }
588
608
  if (apiRoutes.length > 0) {
589
609
  console.log(' \x1b[36mAPI routes:\x1b[0m')
590
610
  apiRoutes.forEach((r) => {
@@ -600,9 +620,6 @@ export const build = async (cliArgs) => {
600
620
  '-c': '--config',
601
621
  }, { argv: cliArgs })
602
622
 
603
- console.log('[@ossy/app][build] Starting...')
604
-
605
-
606
623
  const scriptDir = path.dirname(url.fileURLToPath(import.meta.url))
607
624
  const buildPath = path.resolve('build')
608
625
  const srcDir = path.resolve('src')
@@ -611,7 +628,10 @@ export const build = async (cliArgs) => {
611
628
 
612
629
  resetOssyBuildDir(buildPath)
613
630
 
614
- writeResourceTemplatesBarrelIfPresent({ cwd: process.cwd(), log: true })
631
+ const resourceTemplatesResult = writeResourceTemplatesBarrelIfPresent({
632
+ cwd: process.cwd(),
633
+ log: false,
634
+ })
615
635
 
616
636
  const pagesGeneratedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_PAGES_BASENAME)
617
637
 
@@ -634,14 +654,6 @@ export const build = async (cliArgs) => {
634
654
  let middlewareSourcePath = path.resolve('src/middleware.js');
635
655
  const publicDir = path.resolve('public')
636
656
 
637
- printBuildOverview({
638
- pagesSourcePath: pagesGeneratedPath,
639
- apiSourcePath,
640
- apiOverviewFiles,
641
- configPath,
642
- pageFiles,
643
- });
644
-
645
657
  if (!fs.existsSync(middlewareSourcePath)) {
646
658
  middlewareSourcePath = path.resolve(scriptDir, 'middleware.js')
647
659
  }
@@ -654,21 +666,82 @@ export const build = async (cliArgs) => {
654
666
  const apiBundlePath = path.join(ossyDir, OSSY_API_SERVER_BUNDLE)
655
667
  const tasksBundlePath = path.join(ossyDir, OSSY_TASKS_SERVER_BUNDLE)
656
668
 
669
+ const useDashboard = Object.keys(clientHydrateInput).length > 0
670
+ const overviewSnap = getBuildOverviewSnapshot({
671
+ pagesSourcePath: pagesGeneratedPath,
672
+ apiSourcePath,
673
+ apiOverviewFiles,
674
+ configPath,
675
+ pageFiles,
676
+ })
677
+ const idToPath = Object.fromEntries(
678
+ pageFiles.map((f) => [
679
+ clientHydrateIdForPage(f, srcDir),
680
+ filePathToRoute(f, srcDir).path,
681
+ ])
682
+ )
683
+ const pageIds = useDashboard
684
+ ? [...new Set(pageFiles.map((f) => clientHydrateIdForPage(f, srcDir)))].sort()
685
+ : []
686
+
687
+ let dashboard = null
688
+ if (useDashboard && pageIds.length > 0) {
689
+ dashboard = createBuildDashboard({
690
+ mode: 'full',
691
+ pageIds,
692
+ idToPath,
693
+ overview: {
694
+ title: '@ossy/app build',
695
+ configRel: overviewSnap.configRel,
696
+ apiRoutes: overviewSnap.apiRoutes,
697
+ },
698
+ })
699
+ dashboard.start()
700
+ if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
701
+ dashboard.pushLog(
702
+ `[resource-templates] merged ${resourceTemplatesResult.count} → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
703
+ )
704
+ }
705
+ } else {
706
+ console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild\x1b[0m')
707
+ printBuildOverview({
708
+ pagesSourcePath: pagesGeneratedPath,
709
+ apiSourcePath,
710
+ apiOverviewFiles,
711
+ configPath,
712
+ pageFiles,
713
+ })
714
+ if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
715
+ console.log(
716
+ `[@ossy/app][resource-templates] merged ${resourceTemplatesResult.count} template(s) → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
717
+ )
718
+ }
719
+ }
720
+
721
+ const warnSink = dashboard
722
+ ? (w) => {
723
+ dashboard.pushLog(`rollup: ${w.message}`)
724
+ }
725
+ : undefined
726
+
657
727
  await bundleOssyNodeEntry({
658
728
  inputPath: pagesGeneratedPath,
659
729
  outputFile: pagesBundlePath,
660
730
  nodeEnv: 'production',
731
+ onWarn: warnSink,
661
732
  })
662
733
  await bundleOssyNodeEntry({
663
734
  inputPath: apiSourcePath,
664
735
  outputFile: apiBundlePath,
665
736
  nodeEnv: 'production',
737
+ onWarn: warnSink,
666
738
  })
667
739
  const { taskSourcePath } = resolveTaskSource({ srcDir, buildPath })
668
740
  await bundleOssyNodeEntry({
669
741
  inputPath: taskSourcePath,
670
742
  outputFile: tasksBundlePath,
671
743
  nodeEnv: 'production',
744
+ onWarn: warnSink,
672
745
  })
673
746
 
674
747
  writeAppRuntimeShims({
@@ -677,20 +750,28 @@ export const build = async (cliArgs) => {
677
750
  })
678
751
  copyOssyAppRuntime({ scriptDir, buildPath })
679
752
 
680
- if (Object.keys(clientHydrateInput).length > 0) {
681
- await prerenderReactTask.handler({
682
- op: 'runProduction',
683
- clientHydrateInput,
684
- pageFilesLength: pageFiles.length,
685
- copyPublicFrom: publicDir,
686
- buildPath,
687
- nodeEnv: 'production',
688
- pagesBundlePath,
689
- configSourcePath,
690
- createClientRollupPlugins: createOssyClientRollupPlugins,
691
- minifyBrowserStaticChunks,
692
- })
753
+ if (useDashboard && dashboard) {
754
+ try {
755
+ await prerenderReactTask.handler({
756
+ op: 'runProduction',
757
+ clientHydrateInput,
758
+ pageFilesLength: pageFiles.length,
759
+ copyPublicFrom: publicDir,
760
+ buildPath,
761
+ nodeEnv: 'production',
762
+ pagesBundlePath,
763
+ configSourcePath,
764
+ createClientRollupPlugins: createOssyClientRollupPlugins,
765
+ minifyBrowserStaticChunks,
766
+ reporter: dashboard,
767
+ })
768
+ } finally {
769
+ dashboard.dispose()
770
+ }
693
771
  }
694
772
 
695
- console.log('[@ossy/app][build] Finished');
773
+ if (useDashboard && dashboard) {
774
+ console.log(' \x1b[2m────────────────────────────────────────\x1b[0m')
775
+ }
776
+ console.log(' \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild finished\x1b[0m\n')
696
777
  };
package/cli/dev.js CHANGED
@@ -3,6 +3,8 @@ import url from 'url';
3
3
  import fs from 'fs';
4
4
  import {
5
5
  printBuildOverview,
6
+ getBuildOverviewSnapshot,
7
+ filePathToRoute,
6
8
  discoverFilesByPattern,
7
9
  PAGE_FILE_PATTERN,
8
10
  generatePagesModule,
@@ -29,14 +31,13 @@ import {
29
31
  OSSY_RESOURCE_TEMPLATES_OUT,
30
32
  } from './build.js';
31
33
  import prerenderReactTask from './prerender-react.task.js'
34
+ import { createBuildDashboard } from './build-terminal.js'
32
35
  import { watch } from 'rollup';
33
36
  import arg from 'arg'
34
37
  import { spawn } from 'node:child_process'
35
38
  // import inject from '@rollup/plugin-inject'
36
39
 
37
40
  export const dev = async (cliArgs) => {
38
- console.log('[@ossy/app][dev] Starting...')
39
-
40
41
  const options = arg({
41
42
  '--config': String,
42
43
  '-c': '--config',
@@ -51,7 +52,11 @@ export const dev = async (cliArgs) => {
51
52
 
52
53
  resetOssyBuildDir(buildPath)
53
54
 
54
- writeResourceTemplatesBarrelIfPresent({ cwd: process.cwd(), log: true })
55
+ const resourceTemplatesResult = writeResourceTemplatesBarrelIfPresent({
56
+ cwd: process.cwd(),
57
+ log: false,
58
+ })
59
+ let resourceTemplatesDevLogged = false
55
60
 
56
61
  const pagesGeneratedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_PAGES_BASENAME)
57
62
  fs.writeFileSync(
@@ -74,13 +79,21 @@ export const dev = async (cliArgs) => {
74
79
  let middlewareSourcePath = path.resolve(options['--middleware-source'] || 'src/middleware.js');
75
80
  const publicDir = path.resolve('public')
76
81
 
77
- printBuildOverview({
78
- pagesSourcePath: pagesGeneratedPath,
79
- apiSourcePath,
80
- apiOverviewFiles,
81
- configPath,
82
- pageFiles,
83
- });
82
+ if (pageFiles.length === 0) {
83
+ console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mdev\x1b[0m')
84
+ printBuildOverview({
85
+ pagesSourcePath: pagesGeneratedPath,
86
+ apiSourcePath,
87
+ apiOverviewFiles,
88
+ configPath,
89
+ pageFiles,
90
+ })
91
+ if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
92
+ console.log(
93
+ `[@ossy/app][resource-templates] merged ${resourceTemplatesResult.count} template(s) → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
94
+ )
95
+ }
96
+ }
84
97
 
85
98
  if (!fs.existsSync(middlewareSourcePath)) {
86
99
  middlewareSourcePath = path.resolve(scriptDir, 'middleware.js')
@@ -225,13 +238,15 @@ export const dev = async (cliArgs) => {
225
238
 
226
239
  watcher.on('event', async (event) => {
227
240
  if (event.code === 'BUNDLE_START') {
228
- console.log('[@ossy/app][dev] Building...')
241
+ console.log(' \x1b[2m◐\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mrollup watch …\x1b[0m')
229
242
  }
230
243
  if (event.code === 'ERROR') {
231
- console.error('[@ossy/app][dev] Build error', event.error)
244
+ console.error(' \x1b[31m✖\x1b[0m \x1b[1m@ossy/app\x1b[0m build error', event.error)
232
245
  }
233
246
  if (event.code === 'BUNDLE_END') {
234
- console.log(`[@ossy/app][dev] Built in ${event.duration}ms`)
247
+ console.log(
248
+ ` \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbundles\x1b[0m \x1b[2m(${event.duration}ms)\x1b[0m`
249
+ )
235
250
  }
236
251
  if (event.code === 'END') {
237
252
  writeAppRuntimeShims({
@@ -240,15 +255,58 @@ export const dev = async (cliArgs) => {
240
255
  })
241
256
  copyOssyAppRuntime({ scriptDir, buildPath })
242
257
  if (pageFiles.length > 0) {
258
+ const pageIds = [
259
+ ...new Set(pageFiles.map((f) => clientHydrateIdForPage(f, srcDir))),
260
+ ].sort()
261
+ const overviewSnap = getBuildOverviewSnapshot({
262
+ pagesSourcePath: pagesGeneratedPath,
263
+ apiSourcePath,
264
+ apiOverviewFiles,
265
+ configPath,
266
+ pageFiles,
267
+ })
268
+ const idToPath = Object.fromEntries(
269
+ pageFiles.map((f) => [
270
+ clientHydrateIdForPage(f, srcDir),
271
+ filePathToRoute(f, srcDir).path,
272
+ ])
273
+ )
274
+ const reporter = pageIds.length
275
+ ? createBuildDashboard({
276
+ mode: 'prerender-only',
277
+ pageIds,
278
+ idToPath,
279
+ overview: {
280
+ title: '@ossy/app dev',
281
+ configRel: overviewSnap.configRel,
282
+ apiRoutes: overviewSnap.apiRoutes,
283
+ },
284
+ })
285
+ : null
286
+ reporter?.start()
287
+ if (
288
+ !resourceTemplatesDevLogged &&
289
+ resourceTemplatesResult.wrote &&
290
+ resourceTemplatesResult.path &&
291
+ reporter
292
+ ) {
293
+ reporter.pushLog(
294
+ `[resource-templates] merged ${resourceTemplatesResult.count} → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
295
+ )
296
+ resourceTemplatesDevLogged = true
297
+ }
243
298
  try {
244
299
  await prerenderReactTask.handler({
245
300
  op: 'prerenderPagesParallel',
246
301
  pagesBundlePath,
247
302
  configSourcePath,
248
303
  publicDir: path.join(buildPath, 'public'),
304
+ reporter,
249
305
  })
250
306
  } catch (err) {
251
- console.error('[@ossy/app][dev] Prerender failed', err)
307
+ console.error(' \x1b[31m✖\x1b[0m \x1b[1m@ossy/app\x1b[0m prerender failed', err)
308
+ } finally {
309
+ reporter?.dispose()
252
310
  }
253
311
  }
254
312
  scheduleRestart()
@@ -297,7 +355,7 @@ export const dev = async (cliArgs) => {
297
355
  }
298
356
  const norm = filename.replace(/\\/g, '/')
299
357
  if (/\.resource\.js$/.test(norm) && norm.includes('resource-templates/')) {
300
- writeResourceTemplatesBarrelIfPresent({ cwd: process.cwd(), log: true })
358
+ writeResourceTemplatesBarrelIfPresent({ cwd: process.cwd(), log: false })
301
359
  const rtOut = path.join(resourceTemplatesDir(process.cwd()), OSSY_RESOURCE_TEMPLATES_OUT)
302
360
  if (fs.existsSync(rtOut) && typeof watcher?.invalidate === 'function') {
303
361
  watcher.invalidate(rtOut)
@@ -3,6 +3,7 @@ import url from 'url'
3
3
  import fs from 'fs'
4
4
  import { rollup } from 'rollup'
5
5
  import { BuildPage } from './render-page.task.js'
6
+ import { pageIdFromHydrateEntryName } from './build-terminal.js'
6
7
 
7
8
  /**
8
9
  * Maps an app route path to the file path express.static will serve for that URL
@@ -103,6 +104,7 @@ async function bundleHydratePagesParallel ({
103
104
  buildPathForPlugins,
104
105
  createClientRollupPlugins,
105
106
  minifyBrowserStaticChunks,
107
+ reporter,
106
108
  }) {
107
109
  copyPublicToBuild({ copyPublicFrom, buildPath })
108
110
  fs.mkdirSync(path.join(buildPath, 'public', 'static'), { recursive: true })
@@ -110,29 +112,37 @@ async function bundleHydratePagesParallel ({
110
112
  const entries = Object.entries(clientHydrateInput)
111
113
 
112
114
  const results = await Promise.allSettled(
113
- entries.map(([entryName, stubPath]) => {
114
- const plugins = createClientRollupPlugins({
115
- nodeEnv,
116
- copyPublicFrom: undefined,
117
- buildPath: buildPathForPlugins,
118
- })
119
- return bundleOneHydratePage({
120
- entryName,
121
- stubPath,
122
- buildPath,
123
- plugins,
124
- minifyPlugin: minifyBrowserStaticChunks(),
125
- })
115
+ entries.map(async ([entryName, stubPath]) => {
116
+ const pageId = pageIdFromHydrateEntryName(entryName)
117
+ const t0 = Date.now()
118
+ reporter?.startBundle?.(pageId)
119
+ try {
120
+ const plugins = createClientRollupPlugins({
121
+ nodeEnv,
122
+ copyPublicFrom: undefined,
123
+ buildPath: buildPathForPlugins,
124
+ })
125
+ await bundleOneHydratePage({
126
+ entryName,
127
+ stubPath,
128
+ buildPath,
129
+ plugins,
130
+ minifyPlugin: minifyBrowserStaticChunks(),
131
+ })
132
+ reporter?.completeBundle?.(pageId, { ok: true, ms: Date.now() - t0 })
133
+ } catch (error) {
134
+ reporter?.completeBundle?.(pageId, {
135
+ ok: false,
136
+ ms: Date.now() - t0,
137
+ error,
138
+ })
139
+ console.error(`[@ossy/app][client-bundle] ${entryName} failed:`, error)
140
+ throw error
141
+ }
126
142
  })
127
143
  )
128
144
 
129
145
  const failures = countSettledFailures(results)
130
- results.forEach((r, i) => {
131
- if (r.status === 'rejected') {
132
- const [entryName] = entries[i]
133
- console.error(`[@ossy/app][client-bundle] ${entryName} failed:`, r.reason)
134
- }
135
- })
136
146
 
137
147
  return { results, failures }
138
148
  }
@@ -157,15 +167,13 @@ async function prerenderOnePage ({
157
167
  const outPath = staticHtmlPathForRoute(route.path, publicDir)
158
168
  fs.mkdirSync(path.dirname(outPath), { recursive: true })
159
169
  fs.writeFileSync(outPath, html, 'utf8')
160
- console.log(
161
- `[@ossy/app][prerender] ${route.path} → ${path.relative(process.cwd(), outPath)}`
162
- )
163
170
  }
164
171
 
165
172
  async function prerenderPagesParallel ({
166
173
  pagesBundlePath,
167
174
  configSourcePath,
168
175
  publicDir,
176
+ reporter,
169
177
  }) {
170
178
  const cfgHref = url.pathToFileURL(path.resolve(configSourcePath)).href
171
179
  const pagesHref = url.pathToFileURL(path.resolve(pagesBundlePath)).href
@@ -180,6 +188,10 @@ async function prerenderPagesParallel ({
180
188
  for (const route of pageList) {
181
189
  if (!route?.element) continue
182
190
  if (!pathIsPrerenderable(route.path)) {
191
+ reporter?.skipPrerender?.(
192
+ route.id,
193
+ `not static (${JSON.stringify(route.path)})`
194
+ )
183
195
  console.warn(
184
196
  `[@ossy/app][prerender] Skipping "${route.id}" (path not prerenderable: ${JSON.stringify(route.path)})`
185
197
  )
@@ -189,25 +201,32 @@ async function prerenderPagesParallel ({
189
201
  }
190
202
 
191
203
  const results = await Promise.allSettled(
192
- routesToRender.map((route) =>
193
- prerenderOnePage({
194
- route,
195
- buildTimeConfig,
196
- pageList,
197
- publicDir,
198
- })
199
- )
204
+ routesToRender.map(async (route) => {
205
+ const t0 = Date.now()
206
+ reporter?.startPrerender?.(route.id)
207
+ try {
208
+ await prerenderOnePage({
209
+ route,
210
+ buildTimeConfig,
211
+ pageList,
212
+ publicDir,
213
+ })
214
+ reporter?.completePrerender?.(route.id, { ok: true, ms: Date.now() - t0 })
215
+ } catch (error) {
216
+ reporter?.completePrerender?.(route.id, {
217
+ ok: false,
218
+ ms: Date.now() - t0,
219
+ error,
220
+ })
221
+ console.error(
222
+ `[@ossy/app][prerender] "${route?.id}" (${route?.path}) failed:`,
223
+ error
224
+ )
225
+ throw error
226
+ }
227
+ })
200
228
  )
201
229
  const failures = countSettledFailures(results)
202
- results.forEach((r, i) => {
203
- if (r.status === 'rejected') {
204
- const route = routesToRender[i]
205
- console.error(
206
- `[@ossy/app][prerender] "${route?.id}" (${route?.path}) failed:`,
207
- r.reason
208
- )
209
- }
210
- })
211
230
 
212
231
  return { results, failures }
213
232
  }
@@ -222,6 +241,7 @@ async function runProduction ({
222
241
  configSourcePath,
223
242
  createClientRollupPlugins,
224
243
  minifyBrowserStaticChunks,
244
+ reporter,
225
245
  }) {
226
246
  if (pageFilesLength === 0 || Object.keys(clientHydrateInput).length === 0) {
227
247
  return { bundleFailures: 0, prerenderFailures: 0 }
@@ -235,12 +255,14 @@ async function runProduction ({
235
255
  buildPathForPlugins: buildPath,
236
256
  createClientRollupPlugins,
237
257
  minifyBrowserStaticChunks,
258
+ reporter,
238
259
  })
239
260
 
240
261
  const { failures: prerenderFailures } = await prerenderPagesParallel({
241
262
  pagesBundlePath,
242
263
  configSourcePath,
243
264
  publicDir: path.join(buildPath, 'public'),
265
+ reporter,
244
266
  })
245
267
 
246
268
  if (bundleFailures + prerenderFailures > 0) {
@@ -261,7 +283,12 @@ export default {
261
283
  return runProduction(input)
262
284
  }
263
285
  if (op === 'prerenderPagesParallel') {
264
- return prerenderPagesParallel(input)
286
+ return prerenderPagesParallel({
287
+ pagesBundlePath: input.pagesBundlePath,
288
+ configSourcePath: input.configSourcePath,
289
+ publicDir: input.publicDir,
290
+ reporter: input.reporter,
291
+ })
265
292
  }
266
293
  throw new Error(
267
294
  `[@ossy/app][prerender-react] Unknown op: ${String(op)} (expected runProduction | prerenderPagesParallel)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "1.10.1",
3
+ "version": "1.11.1",
4
4
  "description": "",
5
5
  "source": "./src/index.js",
6
6
  "main": "./src/index.js",
@@ -27,14 +27,14 @@
27
27
  "@babel/eslint-parser": "^7.15.8",
28
28
  "@babel/preset-react": "^7.26.3",
29
29
  "@babel/register": "^7.25.9",
30
- "@ossy/connected-components": "^1.10.1",
31
- "@ossy/design-system": "^1.10.1",
32
- "@ossy/pages": "^1.10.1",
33
- "@ossy/router": "^1.10.1",
34
- "@ossy/router-react": "^1.10.1",
35
- "@ossy/sdk": "^1.10.1",
36
- "@ossy/sdk-react": "^1.10.1",
37
- "@ossy/themes": "^1.10.1",
30
+ "@ossy/connected-components": "^1.11.1",
31
+ "@ossy/design-system": "^1.11.1",
32
+ "@ossy/pages": "^1.11.1",
33
+ "@ossy/router": "^1.11.1",
34
+ "@ossy/router-react": "^1.11.1",
35
+ "@ossy/sdk": "^1.11.1",
36
+ "@ossy/sdk-react": "^1.11.1",
37
+ "@ossy/themes": "^1.11.1",
38
38
  "@rollup/plugin-alias": "^6.0.0",
39
39
  "@rollup/plugin-babel": "6.1.0",
40
40
  "@rollup/plugin-commonjs": "^29.0.0",
@@ -67,5 +67,5 @@
67
67
  "README.md",
68
68
  "tsconfig.json"
69
69
  ],
70
- "gitHead": "d1148a29edb70bdf5daec5571504268723c9d669"
70
+ "gitHead": "dfa200a3ea12c726db2054a0ab902844507716a0"
71
71
  }