@ossy/app 1.10.1 → 1.11.0

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,210 @@
1
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
2
+
3
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`
4
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`
5
+ const green = (s) => `\x1b[32m${s}\x1b[0m`
6
+ const red = (s) => `\x1b[31m${s}\x1b[0m`
7
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`
8
+
9
+ function stripAnsi (s) {
10
+ return String(s).replace(/\x1b\[[0-9;]*m/g, '')
11
+ }
12
+
13
+ function padVisible (s, width) {
14
+ const n = stripAnsi(s).length
15
+ return n >= width ? s : s + ' '.repeat(width - n)
16
+ }
17
+
18
+ export function pageIdFromHydrateEntryName (entryName) {
19
+ const p = String(entryName)
20
+ return p.startsWith('hydrate__') ? p.slice('hydrate__'.length) : p
21
+ }
22
+
23
+ function fmtPart (part, shortLabel, frame) {
24
+ if (part.s === 'idle') return dim('—')
25
+ if (part.s === 'running') {
26
+ const sp = SPINNER[frame % SPINNER.length]
27
+ return `${yellow(sp)} ${dim(shortLabel + '…')}`
28
+ }
29
+ if (part.s === 'ok') return `${green('✔')} ${dim(`${part.ms}ms`)}`
30
+ if (part.s === 'fail') return `${red('✖')} ${dim(shortLabel)}`
31
+ if (part.s === 'skip') return dim(part.note ? `— ${part.note}` : '—')
32
+ return dim('—')
33
+ }
34
+
35
+ function rowLeadIcon (r, mode) {
36
+ if (mode === 'prerender-only') {
37
+ const p = r.prerender.s
38
+ if (p === 'fail') return red('✖')
39
+ if (p === 'running') return yellow('◐')
40
+ if (p === 'ok' || p === 'skip') return green('✔')
41
+ if (p === 'idle') return dim('·')
42
+ return green('✔')
43
+ }
44
+ const b = r.bundle.s
45
+ const p = r.prerender.s
46
+ if (b === 'fail' || p === 'fail') return red('✖')
47
+ if (b === 'running' || p === 'running') return yellow('◐')
48
+ if (b === 'ok' && p === 'idle') return yellow('◐')
49
+ if (b === 'ok' && (p === 'ok' || p === 'skip')) return green('✔')
50
+ if (b === 'idle' && p === 'idle') return dim('·')
51
+ return green('✔')
52
+ }
53
+
54
+ /**
55
+ * Lerna-style live table: one row per page, hydrate + prerender columns.
56
+ * TTY: redraws in place with a spinner while work runs. Non-TTY: one line per finished step.
57
+ */
58
+ export function createParallelPagesReporter ({
59
+ scope = '@ossy/app',
60
+ pageIds,
61
+ stream = process.stdout,
62
+ /** `'full'`: hydrate + prerender columns. `'prerender-only'`: dev watch (client built separately). */
63
+ mode = 'full',
64
+ } = {}) {
65
+ const ids = [...pageIds].sort()
66
+ const maxId = Math.max(8, ...ids.map((id) => String(id).length))
67
+ const tty =
68
+ stream.isTTY === true &&
69
+ process.env.TERM !== 'dumb' &&
70
+ process.env.CI !== 'true' &&
71
+ process.env.OSSY_PLAIN_OUTPUT !== '1'
72
+
73
+ /** @type {Map<string, { bundle: object, prerender: object }>} */
74
+ const rows = new Map()
75
+ for (const id of ids) {
76
+ rows.set(id, {
77
+ bundle: { s: 'idle' },
78
+ prerender: { s: 'idle' },
79
+ })
80
+ }
81
+
82
+ let frame = 0
83
+ let spinTimer = null
84
+ let blockLines = 0
85
+
86
+ const anyRunning = () => {
87
+ for (const r of rows.values()) {
88
+ if (r.bundle.s === 'running' || r.prerender.s === 'running') return true
89
+ }
90
+ return false
91
+ }
92
+
93
+ function ensureSpin () {
94
+ if (!tty || spinTimer) return
95
+ spinTimer = setInterval(() => {
96
+ frame += 1
97
+ redraw()
98
+ }, 80)
99
+ }
100
+
101
+ function stopSpin () {
102
+ if (spinTimer) {
103
+ clearInterval(spinTimer)
104
+ spinTimer = null
105
+ }
106
+ }
107
+
108
+ function lineFor (id) {
109
+ const r = rows.get(id)
110
+ const lead = rowLeadIcon(r, mode)
111
+ const idCol = padVisible(dim(String(id)), maxId)
112
+ const p = fmtPart(r.prerender, 'html', frame)
113
+ if (mode === 'prerender-only') {
114
+ return ` ${lead} ${dim(scope)} ${idCol} ${p}`
115
+ }
116
+ const b = fmtPart(r.bundle, 'hydrate', frame)
117
+ return ` ${lead} ${dim(scope)} ${idCol} ${padVisible(b, 24)} ${p}`
118
+ }
119
+
120
+ function redraw () {
121
+ if (!tty) return
122
+ const lines = ids.map((id) => lineFor(id))
123
+ if (blockLines === 0) {
124
+ for (const ln of lines) stream.write(`${ln}\n`)
125
+ blockLines = lines.length
126
+ return
127
+ }
128
+ stream.write(`\x1b[${blockLines}A`)
129
+ for (const ln of lines) {
130
+ stream.write(`\x1b[2K\r${ln}\n`)
131
+ }
132
+ }
133
+
134
+ function nonTtyLine (id, phase, ok, ms, err) {
135
+ const label = phase === 'bundle' ? 'hydrate' : 'prerender'
136
+ const icon = ok ? green('✔') : red('✖')
137
+ const time = ms != null ? dim(` (${ms}ms)`) : ''
138
+ stream.write(`${icon} ${dim(`${scope}:${label}:`)}${id}${time}\n`)
139
+ if (err && !ok) stream.write(` ${red(String(err.message || err))}\n`)
140
+ }
141
+
142
+ return {
143
+ startBundle (pageId) {
144
+ if (mode === 'prerender-only') return
145
+ const r = rows.get(pageId)
146
+ if (!r) return
147
+ r.bundle = { s: 'running' }
148
+ ensureSpin()
149
+ redraw()
150
+ },
151
+
152
+ completeBundle (pageId, { ok, ms, error }) {
153
+ if (mode === 'prerender-only') return
154
+ const r = rows.get(pageId)
155
+ if (!r) return
156
+ r.bundle = ok ? { s: 'ok', ms } : { s: 'fail', ms, error }
157
+ if (!ok) {
158
+ r.prerender = { s: 'skip', note: 'bundle failed' }
159
+ if (!tty) nonTtyLine(pageId, 'bundle', false, ms, error)
160
+ }
161
+ if (!anyRunning()) stopSpin()
162
+ redraw()
163
+ },
164
+
165
+ startPrerender (pageId) {
166
+ const r = rows.get(pageId)
167
+ if (!r || r.prerender.s === 'skip') return
168
+ r.prerender = { s: 'running' }
169
+ ensureSpin()
170
+ redraw()
171
+ },
172
+
173
+ completePrerender (pageId, { ok, ms, error }) {
174
+ const r = rows.get(pageId)
175
+ if (!r || r.prerender.s === 'skip') return
176
+ r.prerender = ok ? { s: 'ok', ms } : { s: 'fail', ms, error }
177
+ if (!tty) nonTtyLine(pageId, 'prerender', ok, ms, error)
178
+ if (!anyRunning()) stopSpin()
179
+ redraw()
180
+ },
181
+
182
+ skipPrerender (pageId, note) {
183
+ const r = rows.get(pageId)
184
+ if (!r) return
185
+ if (r.bundle.s === 'fail') return
186
+ r.prerender = { s: 'skip', note: note || 'skipped' }
187
+ if (!tty) stream.write(`${dim('—')} ${scope}:prerender:${pageId} (${note})\n`)
188
+ redraw()
189
+ },
190
+
191
+ /** Stop spinner and leave cursor below the block (TTY). */
192
+ dispose () {
193
+ stopSpin()
194
+ if (tty && blockLines > 0) {
195
+ redraw()
196
+ stream.write('\n')
197
+ blockLines = 0
198
+ }
199
+ },
200
+
201
+ printSectionTitle (title) {
202
+ stopSpin()
203
+ if (tty && blockLines > 0) {
204
+ stream.write('\n')
205
+ blockLines = 0
206
+ }
207
+ stream.write(`\n ${bold(scope)} ${dim(title)}\n`)
208
+ },
209
+ }
210
+ }
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 { createParallelPagesReporter } 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)$/
@@ -600,7 +601,8 @@ export const build = async (cliArgs) => {
600
601
  '-c': '--config',
601
602
  }, { argv: cliArgs })
602
603
 
603
- console.log('[@ossy/app][build] Starting...')
604
+ console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild\x1b[0m')
605
+ console.log(' \x1b[2m────────────────────────────────────────\x1b[0m')
604
606
 
605
607
 
606
608
  const scriptDir = path.dirname(url.fileURLToPath(import.meta.url))
@@ -678,19 +680,32 @@ export const build = async (cliArgs) => {
678
680
  copyOssyAppRuntime({ scriptDir, buildPath })
679
681
 
680
682
  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
- })
683
+ const pageIds = [
684
+ ...new Set(pageFiles.map((f) => clientHydrateIdForPage(f, srcDir))),
685
+ ].sort()
686
+ const reporter = pageIds.length
687
+ ? createParallelPagesReporter({ pageIds })
688
+ : null
689
+ reporter?.printSectionTitle('client hydrate + prerender (parallel)')
690
+ try {
691
+ await prerenderReactTask.handler({
692
+ op: 'runProduction',
693
+ clientHydrateInput,
694
+ pageFilesLength: pageFiles.length,
695
+ copyPublicFrom: publicDir,
696
+ buildPath,
697
+ nodeEnv: 'production',
698
+ pagesBundlePath,
699
+ configSourcePath,
700
+ createClientRollupPlugins: createOssyClientRollupPlugins,
701
+ minifyBrowserStaticChunks,
702
+ reporter,
703
+ })
704
+ } finally {
705
+ reporter?.dispose()
706
+ }
693
707
  }
694
708
 
695
- console.log('[@ossy/app][build] Finished');
709
+ console.log(' \x1b[2m────────────────────────────────────────\x1b[0m')
710
+ console.log(' \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild finished\x1b[0m\n')
696
711
  };
package/cli/dev.js CHANGED
@@ -29,13 +29,15 @@ import {
29
29
  OSSY_RESOURCE_TEMPLATES_OUT,
30
30
  } from './build.js';
31
31
  import prerenderReactTask from './prerender-react.task.js'
32
+ import { createParallelPagesReporter } from './build-terminal.js'
32
33
  import { watch } from 'rollup';
33
34
  import arg from 'arg'
34
35
  import { spawn } from 'node:child_process'
35
36
  // import inject from '@rollup/plugin-inject'
36
37
 
37
38
  export const dev = async (cliArgs) => {
38
- console.log('[@ossy/app][dev] Starting...')
39
+ console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mdev\x1b[0m')
40
+ console.log(' \x1b[2m────────────────────────────────────────\x1b[0m')
39
41
 
40
42
  const options = arg({
41
43
  '--config': String,
@@ -225,13 +227,15 @@ export const dev = async (cliArgs) => {
225
227
 
226
228
  watcher.on('event', async (event) => {
227
229
  if (event.code === 'BUNDLE_START') {
228
- console.log('[@ossy/app][dev] Building...')
230
+ console.log(' \x1b[2m◐\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mrollup watch …\x1b[0m')
229
231
  }
230
232
  if (event.code === 'ERROR') {
231
- console.error('[@ossy/app][dev] Build error', event.error)
233
+ console.error(' \x1b[31m✖\x1b[0m \x1b[1m@ossy/app\x1b[0m build error', event.error)
232
234
  }
233
235
  if (event.code === 'BUNDLE_END') {
234
- console.log(`[@ossy/app][dev] Built in ${event.duration}ms`)
236
+ console.log(
237
+ ` \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbundles\x1b[0m \x1b[2m(${event.duration}ms)\x1b[0m`
238
+ )
235
239
  }
236
240
  if (event.code === 'END') {
237
241
  writeAppRuntimeShims({
@@ -240,15 +244,25 @@ export const dev = async (cliArgs) => {
240
244
  })
241
245
  copyOssyAppRuntime({ scriptDir, buildPath })
242
246
  if (pageFiles.length > 0) {
247
+ const pageIds = [
248
+ ...new Set(pageFiles.map((f) => clientHydrateIdForPage(f, srcDir))),
249
+ ].sort()
250
+ const reporter = pageIds.length
251
+ ? createParallelPagesReporter({ pageIds, mode: 'prerender-only' })
252
+ : null
253
+ reporter?.printSectionTitle('prerender (parallel)')
243
254
  try {
244
255
  await prerenderReactTask.handler({
245
256
  op: 'prerenderPagesParallel',
246
257
  pagesBundlePath,
247
258
  configSourcePath,
248
259
  publicDir: path.join(buildPath, 'public'),
260
+ reporter,
249
261
  })
250
262
  } catch (err) {
251
- console.error('[@ossy/app][dev] Prerender failed', err)
263
+ console.error(' \x1b[31m✖\x1b[0m \x1b[1m@ossy/app\x1b[0m prerender failed', err)
264
+ } finally {
265
+ reporter?.dispose()
252
266
  }
253
267
  }
254
268
  scheduleRestart()
@@ -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.0",
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.0",
31
+ "@ossy/design-system": "^1.11.0",
32
+ "@ossy/pages": "^1.11.0",
33
+ "@ossy/router": "^1.11.0",
34
+ "@ossy/router-react": "^1.11.0",
35
+ "@ossy/sdk": "^1.11.0",
36
+ "@ossy/sdk-react": "^1.11.0",
37
+ "@ossy/themes": "^1.11.0",
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": "a151faf7a9af599b885ea98137575978fa1dffae"
71
71
  }