@ossy/app 1.11.0 → 1.11.2

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/cli/build.js CHANGED
@@ -12,7 +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
+ import { createBuildDashboard } from './build-terminal.js'
16
16
 
17
17
  export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
18
18
  export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
@@ -53,15 +53,35 @@ export function minifyBrowserStaticChunks () {
53
53
 
54
54
  /** Subfolder under `build/` for generated pages/api/task entry stubs. */
55
55
  export const OSSY_GEN_DIRNAME = '.ossy'
56
- export const OSSY_GEN_PAGES_BASENAME = 'pages.generated.jsx'
57
- export const OSSY_GEN_API_BASENAME = 'api.generated.js'
58
- export const OSSY_GEN_TASKS_BASENAME = 'tasks.generated.js'
59
-
60
- /** Bundled pages + React for build-time prerender only (not loaded by `server.js`). */
61
- export const OSSY_PAGES_PRERENDER_BUNDLE = 'pages.prerender.bundle.js'
62
- export const OSSY_API_SERVER_BUNDLE = 'api.bundle.js'
63
- export const OSSY_TASKS_SERVER_BUNDLE = 'tasks.bundle.js'
56
+ /** JSON-only manifests (sources + route metadata). */
57
+ export const OSSY_GEN_PAGES_BASENAME = 'pages.generated.json'
58
+ export const OSSY_GEN_API_BASENAME = 'api.generated.json'
59
+ export const OSSY_GEN_TASKS_BASENAME = 'tasks.generated.json'
60
+ /** JSON-only build artifact index (compiled module paths, aligned to generated manifests). */
61
+ export const OSSY_PAGES_BUNDLE_BASENAME = 'pages.bundle.json'
62
+ export const OSSY_API_BUNDLE_BASENAME = 'api.bundle.json'
63
+ export const OSSY_TASKS_BUNDLE_BASENAME = 'tasks.bundle.json'
64
+ /** Small Node loaders (not JSON) that `import()` compiled modules from the bundle manifests. */
65
+ export const OSSY_PAGES_RUNTIME_BASENAME = 'pages.runtime.mjs'
66
+ export const OSSY_API_RUNTIME_BASENAME = 'api.runtime.mjs'
67
+ export const OSSY_TASKS_RUNTIME_BASENAME = 'tasks.runtime.mjs'
68
+
69
+ export const OSSY_PAGE_MODULES_DIRNAME = 'page-modules'
70
+ export const OSSY_API_MODULES_DIRNAME = 'api-modules'
71
+ export const OSSY_TASK_MODULES_DIRNAME = 'task-modules'
72
+
64
73
  export const OSSY_MIDDLEWARE_RUNTIME_BASENAME = 'middleware.runtime.js'
74
+ export const OSSY_SERVER_CONFIG_RUNTIME_BASENAME = 'server-config.runtime.mjs'
75
+ export const OSSY_RENDER_PAGE_RUNTIME_BASENAME = 'render-page.task.js'
76
+
77
+ /** Keep React external across per-page server chunks so `pages.runtime.mjs` shares one React. */
78
+ export const OSSY_PAGE_SERVER_EXTERNAL = [
79
+ 'react',
80
+ 'react-dom',
81
+ 'react-dom/static',
82
+ 'react-dom/client',
83
+ 'react/jsx-runtime',
84
+ ]
65
85
 
66
86
  /** Per-page client entries: `hydrate-<pageId>.jsx` under `.ossy/` */
67
87
  const HYDRATE_STUB_PREFIX = 'hydrate-'
@@ -210,10 +230,18 @@ export function createOssyClientRollupPlugins ({ nodeEnv, copyPublicFrom, buildP
210
230
  }
211
231
 
212
232
  /** Bundles a single Node ESM file (inline dynamic imports) for SSR / API / tasks. */
213
- export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv }) {
233
+ export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv, onWarn, external }) {
214
234
  const bundle = await rollup({
215
235
  input: inputPath,
236
+ ...(external && external.length ? { external } : {}),
216
237
  plugins: createOssyAppBundlePlugins({ nodeEnv }),
238
+ onwarn (warning, defaultHandler) {
239
+ if (onWarn) {
240
+ onWarn(warning)
241
+ return
242
+ }
243
+ defaultHandler(warning)
244
+ },
217
245
  })
218
246
  await bundle.write({
219
247
  file: outputFile,
@@ -224,14 +252,19 @@ export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv }) {
224
252
  }
225
253
 
226
254
  /**
227
- * Re-exports middleware via `file:` URL so `src/middleware.js` can keep relative imports.
255
+ * Re-exports middleware and app config via `file:` URLs so `src/*.js` can keep relative imports.
228
256
  */
229
- export function writeAppRuntimeShims ({ middlewareSourcePath, ossyDir }) {
257
+ export function writeAppRuntimeShims ({ middlewareSourcePath, configSourcePath, ossyDir }) {
230
258
  const mwHref = url.pathToFileURL(path.resolve(middlewareSourcePath)).href
231
259
  fs.writeFileSync(
232
260
  path.join(ossyDir, OSSY_MIDDLEWARE_RUNTIME_BASENAME),
233
261
  `// Generated by @ossy/app — do not edit\nexport { default } from '${mwHref}'\n`
234
262
  )
263
+ const cfgHref = url.pathToFileURL(path.resolve(configSourcePath)).href
264
+ fs.writeFileSync(
265
+ path.join(ossyDir, OSSY_SERVER_CONFIG_RUNTIME_BASENAME),
266
+ `// Generated by @ossy/app — do not edit\nexport { default } from '${cfgHref}'\n`
267
+ )
235
268
  }
236
269
 
237
270
  /**
@@ -251,6 +284,18 @@ export function copyOssyAppRuntime ({ scriptDir, buildPath }) {
251
284
  }
252
285
  fs.copyFileSync(path.join(scriptDir, 'worker-entry.js'), path.join(buildPath, 'worker.js'))
253
286
  fs.copyFileSync(path.join(scriptDir, 'worker-runtime.js'), path.join(buildPath, 'worker-runtime.js'))
287
+ const ossyOut = ossyGeneratedDir(buildPath)
288
+ for (const name of [
289
+ OSSY_PAGES_RUNTIME_BASENAME,
290
+ OSSY_API_RUNTIME_BASENAME,
291
+ OSSY_TASKS_RUNTIME_BASENAME,
292
+ ]) {
293
+ fs.copyFileSync(path.join(scriptDir, name), path.join(ossyOut, name))
294
+ }
295
+ fs.copyFileSync(
296
+ path.join(scriptDir, OSSY_RENDER_PAGE_RUNTIME_BASENAME),
297
+ path.join(ossyOut, OSSY_RENDER_PAGE_RUNTIME_BASENAME)
298
+ )
254
299
  }
255
300
 
256
301
  /**
@@ -274,107 +319,157 @@ export function discoverFilesByPattern (srcDir, filePattern) {
274
319
  return files.sort()
275
320
  }
276
321
 
277
- /**
278
- * Merges every `*.api.js` (and `.api.mjs` / `.api.cjs`) under `src/` into one default export array
279
- * for the Ossy API router ({ id, path, handle }). With no API files, emits `export default []`.
280
- */
281
- export function generateApiModule ({ generatedPath, apiFiles }) {
282
- if (apiFiles.length === 0) {
283
- return [
284
- '// Generated by @ossy/app — do not edit',
285
- '',
286
- 'export default []',
287
- '',
288
- ].join('\n')
322
+ export function writeOssyJson (filePath, data) {
323
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
324
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
325
+ }
326
+
327
+ /** JSON manifest: discovered API source paths (posix, relative to `cwd`). */
328
+ export function buildApiManifestPayload (apiFiles, cwd = process.cwd()) {
329
+ return {
330
+ version: 1,
331
+ files: apiFiles.map((f) => path.relative(cwd, f).replace(/\\/g, '/')),
332
+ }
333
+ }
334
+
335
+ /** JSON manifest: discovered task source paths (posix, relative to `cwd`). */
336
+ export function buildTasksManifestPayload (taskFiles, cwd = process.cwd()) {
337
+ return {
338
+ version: 1,
339
+ files: taskFiles.map((f) => path.relative(cwd, f).replace(/\\/g, '/')),
289
340
  }
290
- const lines = [
291
- '// Generated by @ossy/app — do not edit',
292
- '',
293
- ]
294
- apiFiles.forEach((f, i) => {
295
- const rel = relToGeneratedImport(generatedPath, f)
296
- lines.push(`import * as _api${i} from './${rel}'`)
297
- })
298
- lines.push(
299
- '',
300
- 'function _normalizeApiExport(mod) {',
301
- ' const d = mod?.default',
302
- ' if (d == null) return []',
303
- ' return Array.isArray(d) ? d : [d]',
304
- '}',
305
- '',
306
- 'export default [',
307
- )
308
- const parts = apiFiles.map((_, i) => ` ..._normalizeApiExport(_api${i}),`)
309
- lines.push(parts.join('\n'))
310
- lines.push(']')
311
- lines.push('')
312
- return lines.join('\n')
313
341
  }
314
342
 
315
343
  /**
316
- * Writes `build/.ossy/api.generated.js` and returns its path for `@ossy/api/source-file`.
317
- * Always uses the generated file so the Rollup replace target stays stable.
344
+ * Writes `build/.ossy/api.generated.json` (sources only).
345
+ * Compiled modules + `api.bundle.json` are produced by {@link compileOssyNodeArtifacts}.
318
346
  */
319
- export function resolveApiSource ({ srcDir, buildPath }) {
347
+ export function resolveApiSource ({ srcDir, buildPath, cwd = process.cwd() }) {
320
348
  ensureOssyGeneratedDir(buildPath)
321
349
  const generatedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_API_BASENAME)
322
350
  const apiFiles = discoverFilesByPattern(srcDir, API_FILE_PATTERN)
323
- fs.writeFileSync(
324
- generatedPath,
325
- generateApiModule({ generatedPath, apiFiles })
326
- )
327
- return { apiSourcePath: generatedPath, apiOverviewFiles: apiFiles }
328
- }
329
-
330
- /**
331
- * Merges `src/tasks.js` (optional) and every `*.task.js` under `src/` into one default export array
332
- * of job handlers `{ type, handler }` for the Ossy worker.
333
- */
334
- export function generateTaskModule ({ generatedPath, taskFiles }) {
335
- const lines = [
336
- '// Generated by @ossy/app — do not edit',
337
- '',
338
- ]
339
- taskFiles.forEach((f, i) => {
340
- const rel = relToGeneratedImport(generatedPath, f)
341
- lines.push(`import * as _task${i} from './${rel}'`)
342
- })
343
- lines.push(
344
- '',
345
- 'function _normalizeTaskExport(mod) {',
346
- ' const d = mod?.default',
347
- ' if (d == null) return []',
348
- ' return Array.isArray(d) ? d : [d]',
349
- '}',
350
- '',
351
- 'export default [',
352
- )
353
- const parts = []
354
- taskFiles.forEach((_, i) => {
355
- parts.push(` ..._normalizeTaskExport(_task${i}),`)
356
- })
357
- lines.push(parts.join('\n'))
358
- lines.push(']')
359
- lines.push('')
360
- return lines.join('\n')
351
+ writeOssyJson(generatedPath, buildApiManifestPayload(apiFiles, cwd))
352
+ return { apiGeneratedPath: generatedPath, apiOverviewFiles: apiFiles }
361
353
  }
362
354
 
363
355
  /**
364
- * Writes `build/.ossy/tasks.generated.js` and returns its path (stable Rollup input; empty when no task files).
356
+ * Writes `build/.ossy/tasks.generated.json` (sources only).
365
357
  */
366
- export function resolveTaskSource ({ srcDir, buildPath }) {
358
+ export function resolveTaskSource ({ srcDir, buildPath, cwd = process.cwd() }) {
367
359
  ensureOssyGeneratedDir(buildPath)
368
360
  const generatedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_TASKS_BASENAME)
369
361
  const taskFiles = discoverFilesByPattern(srcDir, TASK_FILE_PATTERN)
370
- fs.writeFileSync(
371
- generatedPath,
372
- generateTaskModule({
373
- generatedPath,
374
- taskFiles,
362
+ writeOssyJson(generatedPath, buildTasksManifestPayload(taskFiles, cwd))
363
+ return { tasksGeneratedPath: generatedPath, taskOverviewFiles: taskFiles }
364
+ }
365
+
366
+ export async function compilePageServerModules ({
367
+ pageFiles,
368
+ srcDir,
369
+ ossyDir,
370
+ nodeEnv,
371
+ onWarn,
372
+ }) {
373
+ const modsDir = path.join(ossyDir, OSSY_PAGE_MODULES_DIRNAME)
374
+ fs.rmSync(modsDir, { recursive: true, force: true })
375
+ if (pageFiles.length === 0) {
376
+ return []
377
+ }
378
+ fs.mkdirSync(modsDir, { recursive: true })
379
+ const bundlePages = []
380
+ for (const f of pageFiles) {
381
+ const pageId = clientHydrateIdForPage(f, srcDir)
382
+ const safeId = String(pageId).replace(/[^a-zA-Z0-9_-]+/g, '-') || 'page'
383
+ const outName = `${safeId}.mjs`
384
+ const outFile = path.join(modsDir, outName)
385
+ await bundleOssyNodeEntry({
386
+ inputPath: f,
387
+ outputFile: outFile,
388
+ nodeEnv,
389
+ onWarn,
390
+ external: OSSY_PAGE_SERVER_EXTERNAL,
375
391
  })
376
- )
377
- return { taskSourcePath: generatedPath, taskOverviewFiles: taskFiles }
392
+ bundlePages.push({
393
+ id: pageId,
394
+ module: `${OSSY_PAGE_MODULES_DIRNAME}/${outName}`,
395
+ })
396
+ }
397
+ return bundlePages
398
+ }
399
+
400
+ export async function compileApiServerModules ({ apiFiles, ossyDir, nodeEnv, onWarn }) {
401
+ const modsDir = path.join(ossyDir, OSSY_API_MODULES_DIRNAME)
402
+ fs.rmSync(modsDir, { recursive: true, force: true })
403
+ if (apiFiles.length === 0) {
404
+ return []
405
+ }
406
+ fs.mkdirSync(modsDir, { recursive: true })
407
+ const modules = []
408
+ for (let i = 0; i < apiFiles.length; i++) {
409
+ const outName = `api-${i}.mjs`
410
+ const outFile = path.join(modsDir, outName)
411
+ await bundleOssyNodeEntry({
412
+ inputPath: apiFiles[i],
413
+ outputFile: outFile,
414
+ nodeEnv,
415
+ onWarn,
416
+ })
417
+ modules.push(`${OSSY_API_MODULES_DIRNAME}/${outName}`)
418
+ }
419
+ return modules
420
+ }
421
+
422
+ export async function compileTaskServerModules ({ taskFiles, ossyDir, nodeEnv, onWarn }) {
423
+ const modsDir = path.join(ossyDir, OSSY_TASK_MODULES_DIRNAME)
424
+ fs.rmSync(modsDir, { recursive: true, force: true })
425
+ if (taskFiles.length === 0) {
426
+ return []
427
+ }
428
+ fs.mkdirSync(modsDir, { recursive: true })
429
+ const modules = []
430
+ for (let i = 0; i < taskFiles.length; i++) {
431
+ const outName = `task-${i}.mjs`
432
+ const outFile = path.join(modsDir, outName)
433
+ await bundleOssyNodeEntry({
434
+ inputPath: taskFiles[i],
435
+ outputFile: outFile,
436
+ nodeEnv,
437
+ onWarn,
438
+ })
439
+ modules.push(`${OSSY_TASK_MODULES_DIRNAME}/${outName}`)
440
+ }
441
+ return modules
442
+ }
443
+
444
+ /**
445
+ * Writes `pages.bundle.json`, `api.bundle.json`, `tasks.bundle.json` by Rollup-compiling each source module.
446
+ */
447
+ export async function compileOssyNodeArtifacts ({
448
+ pageFiles,
449
+ srcDir,
450
+ ossyDir,
451
+ apiFiles,
452
+ taskFiles,
453
+ nodeEnv,
454
+ onWarn,
455
+ }) {
456
+ const [pageBundleList, apiModuleList, taskModuleList] = await Promise.all([
457
+ compilePageServerModules({ pageFiles, srcDir, ossyDir, nodeEnv, onWarn }),
458
+ compileApiServerModules({ apiFiles, ossyDir, nodeEnv, onWarn }),
459
+ compileTaskServerModules({ taskFiles, ossyDir, nodeEnv, onWarn }),
460
+ ])
461
+ writeOssyJson(path.join(ossyDir, OSSY_PAGES_BUNDLE_BASENAME), {
462
+ version: 1,
463
+ pages: pageBundleList,
464
+ })
465
+ writeOssyJson(path.join(ossyDir, OSSY_API_BUNDLE_BASENAME), {
466
+ version: 1,
467
+ modules: apiModuleList,
468
+ })
469
+ writeOssyJson(path.join(ossyDir, OSSY_TASKS_BUNDLE_BASENAME), {
470
+ version: 1,
471
+ modules: taskModuleList,
472
+ })
378
473
  }
379
474
 
380
475
  export function filePathToRoute(filePath, srcDir) {
@@ -407,7 +502,7 @@ export function clientHydrateIdForPage (pageAbsPath, srcDir) {
407
502
 
408
503
  /**
409
504
  * One client entry per page: imports only that page module and hydrates the document.
410
- * Keeps the same `toPage` shape as `generatePagesModule` so SSR and client trees match.
505
+ * Keeps the same `toPage` shape as `pages.runtime.mjs` + manifests so SSR and client trees match.
411
506
  */
412
507
  export function generatePageHydrateModule ({ pageAbsPath, stubAbsPath, srcDir }) {
413
508
  const rel = relToGeneratedImport(stubAbsPath, pageAbsPath)
@@ -484,32 +579,39 @@ export function buildClientHydrateInput (pageFiles, srcDir, ossyDir) {
484
579
  return input
485
580
  }
486
581
 
487
- export function generatePagesModule (pageFiles, srcDir, generatedPath) {
488
- const lines = [
489
- "import React from 'react'",
490
- ...pageFiles.map((f, i) => {
491
- const rel = relToGeneratedImport(generatedPath, f)
492
- return `import * as _page${i} from './${rel}'`
493
- }),
494
- '',
495
- 'function toPage(mod, derived) {',
496
- ' const meta = mod?.metadata || {}',
497
- " const def = mod?.default",
498
- " if (typeof def === 'function') {",
499
- " return { ...derived, ...meta, element: React.createElement(def) }",
500
- ' }',
501
- " return { ...derived, ...meta, ...(def || {}) }",
502
- '}',
503
- '',
504
- 'export default [',
505
- ...pageFiles.map((f, i) => {
506
- const { id, path: defaultPath } = filePathToRoute(f, srcDir)
507
- const pathStr = JSON.stringify(defaultPath)
508
- return ` toPage(_page${i}, { id: '${id}', path: ${pathStr} }),`
509
- }),
510
- ']',
511
- ]
512
- return lines.join('\n')
582
+ /** JSON manifest: route ids, default paths, and page source paths (posix, relative to `cwd`). */
583
+ export function buildPagesGeneratedPayload (pageFiles, srcDir, cwd = process.cwd()) {
584
+ const pages = pageFiles.map((f) => {
585
+ const { path: routePath } = filePathToRoute(f, srcDir)
586
+ const pageId = clientHydrateIdForPage(f, srcDir)
587
+ return {
588
+ id: pageId,
589
+ path: routePath,
590
+ sourceFile: path.relative(cwd, f).replace(/\\/g, '/'),
591
+ }
592
+ })
593
+ return { version: 1, pages }
594
+ }
595
+
596
+ export function writePagesManifest ({
597
+ pageFiles,
598
+ srcDir,
599
+ pagesGeneratedPath,
600
+ cwd = process.cwd(),
601
+ }) {
602
+ writeOssyJson(pagesGeneratedPath, buildPagesGeneratedPayload(pageFiles, srcDir, cwd))
603
+ }
604
+
605
+ export function parsePagesFromManifestJson (manifestPath) {
606
+ try {
607
+ const raw = fs.readFileSync(manifestPath, 'utf8')
608
+ const data = JSON.parse(raw)
609
+ const pages = data?.pages
610
+ if (!Array.isArray(pages)) return []
611
+ return pages.map((p) => ({ id: p.id, path: p.path }))
612
+ } catch {
613
+ return []
614
+ }
513
615
  }
514
616
 
515
617
  export function parsePagesFromSource(filePath) {
@@ -545,24 +647,42 @@ export function parsePagesFromSource(filePath) {
545
647
  }
546
648
  }
547
649
 
548
- export function printBuildOverview({
650
+ /**
651
+ * Same facts as the old build overview printout, for dashboards / plain logging.
652
+ */
653
+ export function getBuildOverviewSnapshot ({
549
654
  pagesSourcePath,
550
- apiSourcePath,
551
655
  apiOverviewFiles = [],
552
656
  configPath,
553
657
  pageFiles,
554
658
  }) {
555
- const rel = (p) => p ? path.relative(process.cwd(), p) : undefined
659
+ const rel = (p) => (p ? path.relative(process.cwd(), p) : undefined)
556
660
  const srcDir = path.resolve(process.cwd(), 'src')
661
+ const configRel = fs.existsSync(configPath) ? rel(configPath) : null
662
+
663
+ const pages = pageFiles?.length
664
+ ? pageFiles.map((f) => {
665
+ const { id, path: routePath } = filePathToRoute(f, srcDir)
666
+ return { id: clientHydrateIdForPage(f, srcDir), path: routePath }
667
+ })
668
+ : parsePagesFromManifestJson(pagesSourcePath)
669
+
670
+ const apiRoutes = []
671
+ for (const f of apiOverviewFiles) {
672
+ if (fs.existsSync(f)) apiRoutes.push(...parsePagesFromSource(f))
673
+ }
674
+
675
+ return { configRel, pages, apiRoutes }
676
+ }
677
+
678
+ export function printBuildOverview (opts) {
679
+ const { configRel, pages, apiRoutes } = getBuildOverviewSnapshot(opts)
557
680
  console.log('\n \x1b[1mBuild overview\x1b[0m')
558
- if (fs.existsSync(configPath)) {
559
- console.log(` \x1b[36mConfig:\x1b[0m ${rel(configPath)}`)
681
+ if (configRel) {
682
+ console.log(` \x1b[36mConfig:\x1b[0m ${configRel}`)
560
683
  }
561
684
  console.log(' ' + '─'.repeat(50))
562
685
 
563
- const pages = pageFiles?.length
564
- ? pageFiles.map((f) => filePathToRoute(f, srcDir))
565
- : parsePagesFromSource(pagesSourcePath)
566
686
  if (pages.length > 0) {
567
687
  console.log(' \x1b[36mRoutes:\x1b[0m')
568
688
  const maxId = Math.max(6, ...pages.map((p) => String(p.id).length))
@@ -576,16 +696,6 @@ export function printBuildOverview({
576
696
  console.log(' \x1b[33mRoutes:\x1b[0m (could not parse or empty)')
577
697
  }
578
698
 
579
- const apiFilesToScan =
580
- apiOverviewFiles?.length > 0
581
- ? apiOverviewFiles
582
- : fs.existsSync(apiSourcePath)
583
- ? [apiSourcePath]
584
- : []
585
- const apiRoutes = []
586
- for (const f of apiFilesToScan) {
587
- if (fs.existsSync(f)) apiRoutes.push(...parsePagesFromSource(f))
588
- }
589
699
  if (apiRoutes.length > 0) {
590
700
  console.log(' \x1b[36mAPI routes:\x1b[0m')
591
701
  apiRoutes.forEach((r) => {
@@ -601,10 +711,6 @@ export const build = async (cliArgs) => {
601
711
  '-c': '--config',
602
712
  }, { argv: cliArgs })
603
713
 
604
- console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild\x1b[0m')
605
- console.log(' \x1b[2m────────────────────────────────────────\x1b[0m')
606
-
607
-
608
714
  const scriptDir = path.dirname(url.fileURLToPath(import.meta.url))
609
715
  const buildPath = path.resolve('build')
610
716
  const srcDir = path.resolve('src')
@@ -613,37 +719,29 @@ export const build = async (cliArgs) => {
613
719
 
614
720
  resetOssyBuildDir(buildPath)
615
721
 
616
- writeResourceTemplatesBarrelIfPresent({ cwd: process.cwd(), log: true })
722
+ const resourceTemplatesResult = writeResourceTemplatesBarrelIfPresent({
723
+ cwd: process.cwd(),
724
+ log: false,
725
+ })
617
726
 
618
- const pagesGeneratedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_PAGES_BASENAME)
727
+ const ossyDir = ossyGeneratedDir(buildPath)
728
+ const pagesGeneratedPath = path.join(ossyDir, OSSY_GEN_PAGES_BASENAME)
619
729
 
620
- fs.writeFileSync(
730
+ writePagesManifest({
731
+ pageFiles,
732
+ srcDir,
621
733
  pagesGeneratedPath,
622
- generatePagesModule(pageFiles, srcDir, pagesGeneratedPath)
623
- )
624
- const ossyDir = ossyGeneratedDir(buildPath)
734
+ })
625
735
  writePageHydrateStubs(pageFiles, srcDir, ossyDir)
626
736
  const clientHydrateInput = buildClientHydrateInput(pageFiles, srcDir, ossyDir)
627
737
 
628
- const {
629
- apiSourcePath: resolvedApi,
630
- apiOverviewFiles,
631
- } = resolveApiSource({
738
+ const { apiOverviewFiles } = resolveApiSource({
632
739
  srcDir,
633
740
  buildPath,
634
741
  })
635
- let apiSourcePath = resolvedApi
636
742
  let middlewareSourcePath = path.resolve('src/middleware.js');
637
743
  const publicDir = path.resolve('public')
638
744
 
639
- printBuildOverview({
640
- pagesSourcePath: pagesGeneratedPath,
641
- apiSourcePath,
642
- apiOverviewFiles,
643
- configPath,
644
- pageFiles,
645
- });
646
-
647
745
  if (!fs.existsSync(middlewareSourcePath)) {
648
746
  middlewareSourcePath = path.resolve(scriptDir, 'middleware.js')
649
747
  }
@@ -652,41 +750,83 @@ export const build = async (cliArgs) => {
652
750
  ? configPath
653
751
  : path.resolve(scriptDir, 'default-config.js')
654
752
 
655
- const pagesBundlePath = path.join(ossyDir, OSSY_PAGES_PRERENDER_BUNDLE)
656
- const apiBundlePath = path.join(ossyDir, OSSY_API_SERVER_BUNDLE)
657
- const tasksBundlePath = path.join(ossyDir, OSSY_TASKS_SERVER_BUNDLE)
753
+ const pagesEntryPath = path.join(ossyDir, OSSY_PAGES_RUNTIME_BASENAME)
658
754
 
659
- await bundleOssyNodeEntry({
660
- inputPath: pagesGeneratedPath,
661
- outputFile: pagesBundlePath,
662
- nodeEnv: 'production',
663
- })
664
- await bundleOssyNodeEntry({
665
- inputPath: apiSourcePath,
666
- outputFile: apiBundlePath,
667
- nodeEnv: 'production',
755
+ const useDashboard = Object.keys(clientHydrateInput).length > 0
756
+ const overviewSnap = getBuildOverviewSnapshot({
757
+ pagesSourcePath: pagesGeneratedPath,
758
+ apiOverviewFiles,
759
+ configPath,
760
+ pageFiles,
668
761
  })
669
- const { taskSourcePath } = resolveTaskSource({ srcDir, buildPath })
670
- await bundleOssyNodeEntry({
671
- inputPath: taskSourcePath,
672
- outputFile: tasksBundlePath,
762
+ const idToPath = Object.fromEntries(
763
+ pageFiles.map((f) => [
764
+ clientHydrateIdForPage(f, srcDir),
765
+ filePathToRoute(f, srcDir).path,
766
+ ])
767
+ )
768
+ const pageIds = useDashboard
769
+ ? [...new Set(pageFiles.map((f) => clientHydrateIdForPage(f, srcDir)))].sort()
770
+ : []
771
+
772
+ let dashboard = null
773
+ if (useDashboard && pageIds.length > 0) {
774
+ dashboard = createBuildDashboard({
775
+ mode: 'full',
776
+ pageIds,
777
+ idToPath,
778
+ overview: {
779
+ title: '@ossy/app build',
780
+ configRel: overviewSnap.configRel,
781
+ apiRoutes: overviewSnap.apiRoutes,
782
+ },
783
+ })
784
+ dashboard.start()
785
+ if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
786
+ dashboard.pushLog(
787
+ `[resource-templates] merged ${resourceTemplatesResult.count} → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
788
+ )
789
+ }
790
+ } else {
791
+ console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild\x1b[0m')
792
+ printBuildOverview({
793
+ pagesSourcePath: pagesGeneratedPath,
794
+ apiOverviewFiles,
795
+ configPath,
796
+ pageFiles,
797
+ })
798
+ if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
799
+ console.log(
800
+ `[@ossy/app][resource-templates] merged ${resourceTemplatesResult.count} template(s) → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
801
+ )
802
+ }
803
+ }
804
+
805
+ const warnSink = dashboard
806
+ ? (w) => {
807
+ dashboard.pushLog(`rollup: ${w.message}`)
808
+ }
809
+ : undefined
810
+
811
+ const { taskOverviewFiles } = resolveTaskSource({ srcDir, buildPath })
812
+ await compileOssyNodeArtifacts({
813
+ pageFiles,
814
+ srcDir,
815
+ ossyDir,
816
+ apiFiles: apiOverviewFiles,
817
+ taskFiles: taskOverviewFiles,
673
818
  nodeEnv: 'production',
819
+ onWarn: warnSink,
674
820
  })
675
821
 
676
822
  writeAppRuntimeShims({
677
823
  middlewareSourcePath,
824
+ configSourcePath,
678
825
  ossyDir,
679
826
  })
680
827
  copyOssyAppRuntime({ scriptDir, buildPath })
681
828
 
682
- if (Object.keys(clientHydrateInput).length > 0) {
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)')
829
+ if (useDashboard && dashboard) {
690
830
  try {
691
831
  await prerenderReactTask.handler({
692
832
  op: 'runProduction',
@@ -695,17 +835,19 @@ export const build = async (cliArgs) => {
695
835
  copyPublicFrom: publicDir,
696
836
  buildPath,
697
837
  nodeEnv: 'production',
698
- pagesBundlePath,
838
+ pagesEntryPath,
699
839
  configSourcePath,
700
840
  createClientRollupPlugins: createOssyClientRollupPlugins,
701
841
  minifyBrowserStaticChunks,
702
- reporter,
842
+ reporter: dashboard,
703
843
  })
704
844
  } finally {
705
- reporter?.dispose()
845
+ dashboard.dispose()
706
846
  }
707
847
  }
708
848
 
709
- console.log(' \x1b[2m────────────────────────────────────────\x1b[0m')
849
+ if (useDashboard && dashboard) {
850
+ console.log(' \x1b[2m────────────────────────────────────────\x1b[0m')
851
+ }
710
852
  console.log(' \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild finished\x1b[0m\n')
711
853
  };