@ossy/app 1.11.7 → 1.11.8

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.
@@ -1,5 +1,3 @@
1
- import util from 'node:util'
2
-
3
1
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
4
2
 
5
3
  const dim = (s) => `\x1b[2m${s}\x1b[0m`
@@ -69,15 +67,10 @@ function useTty (stream) {
69
67
  )
70
68
  }
71
69
 
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
70
  export function createBuildDashboard ({
77
71
  scope = '@ossy/app',
78
72
  mode = 'full',
79
73
  pageIds,
80
- /** @type {Record<string, string>} route path per page id */
81
74
  idToPath = {},
82
75
  overview = { title: '@ossy/app', configRel: null, apiRoutes: [] },
83
76
  stream = process.stdout,
@@ -86,11 +79,7 @@ export function createBuildDashboard ({
86
79
  const maxId = Math.max(6, ...ids.map((id) => String(id).length))
87
80
  const tty = useTty(stream)
88
81
  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
82
 
93
- /** @type {Map<string, { bundle: object, prerender: object }>} */
94
83
  const rows = new Map()
95
84
  for (const id of ids) {
96
85
  rows.set(id, {
@@ -99,13 +88,9 @@ export function createBuildDashboard ({
99
88
  })
100
89
  }
101
90
 
102
- const logBuffer = []
103
- const MAX_LOG = 120
104
91
  let frame = 0
105
92
  let spinTimer = null
106
93
  let blockLines = 0
107
- let captureActive = false
108
- let savedConsole = null
109
94
 
110
95
  const anyRunning = () => {
111
96
  for (const r of rows.values()) {
@@ -114,36 +99,23 @@ export function createBuildDashboard ({
114
99
  return false
115
100
  }
116
101
 
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 () {
102
+ function headerLines () {
130
103
  const out = []
131
104
  out.push(bold(overview.title || `${scope} build`))
132
105
  if (overview.configRel) {
133
- out.push(`${dim('config')} ${truncateVisible(overview.configRel, LEFT_W - 10)}`)
106
+ out.push(`${dim('config')} ${truncateVisible(overview.configRel, termW - 10)}`)
134
107
  }
135
108
  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)
109
+ if (api.length > 0) {
110
+ if (api.length <= 2) {
111
+ for (const r of api) {
112
+ out.push(`${dim('api')} ${truncateVisible(`${r.id} ${r.path}`, termW - 6)}`)
113
+ }
114
+ } else {
115
+ out.push(`${dim('api')} ${api.length} routes`)
142
116
  }
143
- } else {
144
- out.push(`${dim('api')} ${api.length} routes`)
145
117
  }
146
- out.push(dim('─'.repeat(Math.min(LEFT_W - 2, 44))))
118
+ out.push(dim('─'.repeat(Math.min(termW - 2, 44))))
147
119
  return out
148
120
  }
149
121
 
@@ -161,46 +133,20 @@ export function createBuildDashboard ({
161
133
  return ` ${lead} ${dim(scope)} ${idCol} ${pathPad} ${padVisible(b, 20)} ${p}`
162
134
  }
163
135
 
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`
136
+ function buildLines () {
137
+ return [...headerLines(), ...ids.map((id) => lineForPageRow(id))]
185
138
  }
186
139
 
187
140
  function redraw () {
188
141
  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
- }
142
+ const lines = buildLines()
195
143
  if (blockLines === 0) {
196
- for (const ln of lines) stream.write(ln)
144
+ for (const ln of lines) stream.write(ln + '\n')
197
145
  blockLines = lines.length
198
146
  return
199
147
  }
200
148
  stream.write(`\x1b[${blockLines}A`)
201
- for (const ln of lines) {
202
- stream.write(`\x1b[2K\r${ln}`)
203
- }
149
+ for (const ln of lines) stream.write(`\x1b[2K\r${ln}\n`)
204
150
  }
205
151
 
206
152
  function ensureSpin () {
@@ -226,55 +172,13 @@ export function createBuildDashboard ({
226
172
  if (err && !ok) stream.write(` ${red(String(err.message || err))}\n`)
227
173
  }
228
174
 
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
175
  return {
266
- pushLog,
267
-
268
- /** Start optional console hijack + first paint (TTY). Plain header only when not a TTY. */
269
176
  start () {
270
177
  if (!tty) {
271
- for (const ln of buildHeaderOnlyLines()) {
272
- stream.write(`${ln}\n`)
273
- }
178
+ for (const ln of headerLines()) stream.write(`${ln}\n`)
274
179
  stream.write('\n')
275
180
  return
276
181
  }
277
- beginCapture()
278
182
  redraw()
279
183
  },
280
184
 
@@ -326,13 +230,8 @@ export function createBuildDashboard ({
326
230
  redraw()
327
231
  },
328
232
 
329
- printSectionTitle () {
330
- /* merged into overview.title — no-op for API compat */
331
- },
332
-
333
233
  dispose () {
334
234
  stopSpin()
335
- endCapture()
336
235
  if (tty && blockLines > 0) {
337
236
  redraw()
338
237
  stream.write('\n')
package/cli/build.js CHANGED
@@ -87,6 +87,11 @@ export const OSSY_PAGE_SERVER_EXTERNAL = [
87
87
  'react/jsx-runtime',
88
88
  ]
89
89
 
90
+ /** Output directory (relative to buildPath) for per-page SSR bundles. */
91
+ export const OSSY_SSR_DIRNAME = 'ssr'
92
+ /** Temp stub entries for SSR bundles (inside .ossy/). */
93
+ const OSSY_SSR_ENTRIES_DIRNAME = 'ssr-entries'
94
+
90
95
  /** Per-page client entries: `hydrate-<pageId>.jsx` under `.ossy/` */
91
96
  const HYDRATE_STUB_PREFIX = 'hydrate-'
92
97
  const HYDRATE_STUB_SUFFIX = '.jsx'
@@ -200,7 +205,8 @@ export function createOssyAppBundlePlugins ({ nodeEnv }) {
200
205
  }
201
206
 
202
207
  /**
203
- * Rollup plugins for browser hydrate bundles.
208
+ * Rollup plugins for browser hydrate bundles. React and all deps are bundled in so the output
209
+ * is self-contained and requires no import maps or CDN.
204
210
  */
205
211
  export function createOssyClientRollupPlugins ({ nodeEnv, copyPublicFrom, buildPath }) {
206
212
  const plugins = [
@@ -212,7 +218,7 @@ export function createOssyClientRollupPlugins ({ nodeEnv, copyPublicFrom, buildP
212
218
  nodeExternals({
213
219
  deps: false,
214
220
  devDeps: true,
215
- peerDeps: true,
221
+ peerDeps: false,
216
222
  packagePath: path.join(process.cwd(), 'package.json'),
217
223
  }),
218
224
  resolveCommonJsDependencies(),
@@ -233,6 +239,106 @@ export function createOssyClientRollupPlugins ({ nodeEnv, copyPublicFrom, buildP
233
239
  return plugins
234
240
  }
235
241
 
242
+ /**
243
+ * Rollup plugins for per-page SSR bundles: server-side resolution, React and all deps bundled in
244
+ * so the output is self-contained (no node_modules needed at runtime).
245
+ */
246
+ export function createOssySsrBundlePlugins ({ nodeEnv }) {
247
+ return [
248
+ replace({
249
+ preventAssignment: true,
250
+ 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
251
+ }),
252
+ json(),
253
+ nodeExternals({
254
+ deps: false,
255
+ devDeps: true,
256
+ peerDeps: false,
257
+ packagePath: path.join(process.cwd(), 'package.json'),
258
+ }),
259
+ resolveCommonJsDependencies(),
260
+ resolveDependencies({ preferBuiltins: true }),
261
+ babel({
262
+ babelHelpers: 'bundled',
263
+ extensions: ['.jsx', '.tsx'],
264
+ presets: ['@babel/preset-react'],
265
+ }),
266
+ ]
267
+ }
268
+
269
+ /** Generates the SSR entry stub for a page: exports renderPage(props, options) and metadata. */
270
+ export function generatePageSsrModule ({ pageAbsPath, stubAbsPath }) {
271
+ const rel = relToGeneratedImport(stubAbsPath, pageAbsPath)
272
+ return [
273
+ '// Generated by @ossy/app — do not edit',
274
+ '',
275
+ "import { createElement } from 'react'",
276
+ "import { renderToPipeableStream } from 'react-dom/server'",
277
+ "import { Writable } from 'node:stream'",
278
+ `import * as _page from './${rel}'`,
279
+ '',
280
+ 'export const metadata = _page.metadata',
281
+ '',
282
+ 'export function renderPage (props, options = {}) {',
283
+ ' return new Promise((resolve, reject) => {',
284
+ " let html = ''",
285
+ ' const writable = new Writable({',
286
+ ' write (chunk, _enc, cb) { html += chunk.toString(); cb() },',
287
+ ' })',
288
+ ' const { pipe } = renderToPipeableStream(createElement(_page.default, props), {',
289
+ ' ...options,',
290
+ ' onAllReady () { pipe(writable) },',
291
+ ' onError (err) { reject(err) },',
292
+ ' })',
293
+ " writable.on('finish', () => resolve(html))",
294
+ ' })',
295
+ '}',
296
+ '',
297
+ ].join('\n')
298
+ }
299
+
300
+ /** Writes `ssr-entries/<id>.mjs` stubs for each page into ossyDir. */
301
+ export function writePageSsrStubs (pageFiles, srcDir, ossyDir) {
302
+ const entriesDir = path.join(ossyDir, OSSY_SSR_ENTRIES_DIRNAME)
303
+ fs.rmSync(entriesDir, { recursive: true, force: true })
304
+ if (pageFiles.length === 0) return
305
+ fs.mkdirSync(entriesDir, { recursive: true })
306
+ for (const f of pageFiles) {
307
+ const pageId = clientHydrateIdForPage(f, srcDir)
308
+ const stubPath = path.join(entriesDir, `${pageId}.mjs`)
309
+ fs.writeFileSync(stubPath, generatePageSsrModule({ pageAbsPath: f, stubAbsPath: stubPath }))
310
+ }
311
+ }
312
+
313
+ /** Compiles per-page SSR bundles to `build/ssr/<id>.mjs`. Each is self-contained with React bundled in. */
314
+ export async function compilePageSsrModules ({ pageFiles, srcDir, ossyDir, buildPath, nodeEnv, onWarn }) {
315
+ const ssrDir = path.join(buildPath, OSSY_SSR_DIRNAME)
316
+ const entriesDir = path.join(ossyDir, OSSY_SSR_ENTRIES_DIRNAME)
317
+ fs.rmSync(ssrDir, { recursive: true, force: true })
318
+ if (pageFiles.length === 0) return []
319
+ fs.mkdirSync(ssrDir, { recursive: true })
320
+ const plugins = createOssySsrBundlePlugins({ nodeEnv })
321
+ const results = await Promise.all(
322
+ pageFiles.map(async (f) => {
323
+ const pageId = clientHydrateIdForPage(f, srcDir)
324
+ const stubPath = path.join(entriesDir, `${pageId}.mjs`)
325
+ const outFile = path.join(ssrDir, `${pageId}.mjs`)
326
+ const bundle = await rollup({
327
+ input: stubPath,
328
+ plugins,
329
+ onwarn (warning, defaultHandler) {
330
+ if (onWarn) { onWarn(warning); return }
331
+ defaultHandler(warning)
332
+ },
333
+ })
334
+ await bundle.write({ file: outFile, format: 'esm', inlineDynamicImports: true })
335
+ await bundle.close()
336
+ return { id: pageId }
337
+ })
338
+ )
339
+ return results
340
+ }
341
+
236
342
  /** Bundles a single Node ESM file (inline dynamic imports) for SSR / API / tasks. */
237
343
  export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv, onWarn, external }) {
238
344
  const bundle = await rollup({
@@ -455,39 +561,40 @@ export async function compileTaskServerModules ({ taskFiles, ossyDir, nodeEnv, o
455
561
  * Merges compiled `metadata` + `module` into `pages.generated.json` (same order as `pageBundleList`).
456
562
  * Writes `module` on each route for Node SSR (`import()` of `page-modules/*.mjs`); hydrate uses a separate Rollup client entry.
457
563
  */
458
- export async function enrichPagesGeneratedManifest ({
459
- ossyDir,
460
- pagesGeneratedPath,
461
- pageBundleList,
462
- }) {
463
- if (!pageBundleList?.length || !fs.existsSync(pagesGeneratedPath)) return
564
+ /**
565
+ * Enriches `pages.generated.json` with `module` paths pointing to SSR bundles and merges
566
+ * any `metadata` exported by each page. Runs after SSR bundles are compiled.
567
+ */
568
+ export async function enrichPagesGeneratedManifest ({ ossyDir, pagesGeneratedPath }) {
569
+ if (!fs.existsSync(pagesGeneratedPath)) return
464
570
  const raw = JSON.parse(fs.readFileSync(pagesGeneratedPath, 'utf8'))
465
571
  const basePages = raw?.pages
466
- if (!Array.isArray(basePages) || basePages.length !== pageBundleList.length) {
467
- throw new Error(
468
- '[@ossy/app] pages.generated.json page count must match compiled page modules (re-run build).'
469
- )
470
- }
572
+ if (!Array.isArray(basePages) || basePages.length === 0) return
573
+
471
574
  const pages = []
472
- for (let i = 0; i < basePages.length; i++) {
473
- const abs = path.join(ossyDir, pageBundleList[i].module)
474
- const mod = await import(pathToFileURL(abs).href)
475
- const meta = mod?.metadata && typeof mod.metadata === 'object' ? mod.metadata : {}
476
- const derived = { id: basePages[i].id, path: basePages[i].path }
575
+ for (const basePage of basePages) {
576
+ const moduleRelPath = `../ssr/${basePage.id}.mjs`
577
+ const bundleAbs = path.join(ossyDir, moduleRelPath)
578
+ let meta = {}
579
+ if (fs.existsSync(bundleAbs)) {
580
+ try {
581
+ const mod = await import(pathToFileURL(bundleAbs).href)
582
+ meta = mod?.metadata && typeof mod.metadata === 'object' ? mod.metadata : {}
583
+ } catch {
584
+ // metadata unreadable — continue with defaults
585
+ }
586
+ }
477
587
  const merged = {
478
- ...derived,
588
+ id: basePage.id,
589
+ path: basePage.path,
479
590
  ...meta,
480
- sourceFile: basePages[i].sourceFile,
481
- module: pageBundleList[i].module,
591
+ sourceFile: basePage.sourceFile,
592
+ module: moduleRelPath,
482
593
  }
483
594
  try {
484
595
  JSON.stringify(merged)
485
596
  } catch {
486
- pages.push({
487
- ...derived,
488
- sourceFile: basePages[i].sourceFile,
489
- module: pageBundleList[i].module,
490
- })
597
+ pages.push({ id: basePage.id, path: basePage.path, sourceFile: basePage.sourceFile, module: moduleRelPath })
491
598
  continue
492
599
  }
493
600
  pages.push(merged)
@@ -496,26 +603,23 @@ export async function enrichPagesGeneratedManifest ({
496
603
  }
497
604
 
498
605
  /**
499
- * Writes `pages.bundle.json`, `api.bundle.json`, `tasks.bundle.json` by Rollup-compiling each source module.
606
+ * Compiles server-side artifacts: per-page SSR bundles, API modules, and task modules.
500
607
  */
501
608
  export async function compileOssyNodeArtifacts ({
502
609
  pageFiles,
503
610
  srcDir,
504
611
  ossyDir,
612
+ buildPath,
505
613
  apiFiles,
506
614
  taskFiles,
507
615
  nodeEnv,
508
616
  onWarn,
509
617
  }) {
510
- const [pageBundleList, apiModuleList, taskModuleList] = await Promise.all([
511
- compilePageServerModules({ pageFiles, srcDir, ossyDir, nodeEnv, onWarn }),
618
+ const [, apiModuleList, taskModuleList] = await Promise.all([
619
+ compilePageSsrModules({ pageFiles, srcDir, ossyDir, buildPath, nodeEnv, onWarn }),
512
620
  compileApiServerModules({ apiFiles, ossyDir, nodeEnv, onWarn }),
513
621
  compileTaskServerModules({ taskFiles, ossyDir, nodeEnv, onWarn }),
514
622
  ])
515
- writeOssyJson(path.join(ossyDir, OSSY_PAGES_BUNDLE_BASENAME), {
516
- version: 1,
517
- pages: pageBundleList,
518
- })
519
623
  writeOssyJson(path.join(ossyDir, OSSY_API_BUNDLE_BASENAME), {
520
624
  version: 1,
521
625
  modules: apiModuleList,
@@ -527,7 +631,6 @@ export async function compileOssyNodeArtifacts ({
527
631
  await enrichPagesGeneratedManifest({
528
632
  ossyDir,
529
633
  pagesGeneratedPath: path.join(ossyDir, OSSY_GEN_PAGES_BASENAME),
530
- pageBundleList,
531
634
  })
532
635
  }
533
636
 
@@ -598,8 +701,9 @@ export function writePageServerRollupEntry ({ pageAbsPath, stubPath }) {
598
701
  }
599
702
 
600
703
  /**
601
- * One client entry per page: Rollup bundles this stub with the page source so `react` resolves in the browser.
602
- * (Do not `import()` the Node `page-modules/*.mjs` build from the client those files use bare `react` specifiers.)
704
+ * One bundle per page: exports the component (for server SSR import) and auto-hydrates in the browser.
705
+ * React is kept as a bare external import resolved from node_modules on the server and via
706
+ * import map on the client.
603
707
  */
604
708
  export function generatePageHydrateModule ({ pageAbsPath, stubAbsPath, srcDir }) {
605
709
  const rel = relToGeneratedImport(stubAbsPath, pageAbsPath)
@@ -607,17 +711,17 @@ export function generatePageHydrateModule ({ pageAbsPath, stubAbsPath, srcDir })
607
711
  '// Generated by @ossy/app — do not edit',
608
712
  '',
609
713
  "import React, { createElement } from 'react'",
610
- "import 'react-dom'",
611
714
  "import { hydrateRoot } from 'react-dom/client'",
612
715
  `import * as _page from './${rel}'`,
613
716
  '',
614
- 'const initialConfig = window.__INITIAL_APP_CONFIG__ || {}',
615
- 'const Page = _page?.default',
616
- 'if (typeof Page !== \'function\') {',
617
- ' throw new Error(`[@ossy/app] Page must export default as a function component`)',
717
+ 'export default _page.default',
718
+ 'export const metadata = _page.metadata',
719
+ '',
720
+ "if (typeof window !== 'undefined') {",
721
+ " const Page = _page.default",
722
+ " const config = window.__INITIAL_APP_CONFIG__ || {}",
723
+ " hydrateRoot(document, createElement(Page, config))",
618
724
  '}',
619
- 'const rootTree = createElement(Page, initialConfig)',
620
- 'hydrateRoot(document, rootTree)',
621
725
  '',
622
726
  ].join('\n')
623
727
  }
@@ -816,6 +920,7 @@ export const build = async (cliArgs) => {
816
920
  pagesGeneratedPath,
817
921
  })
818
922
  writePageHydrateStubs(pageFiles, srcDir, ossyDir)
923
+ writePageSsrStubs(pageFiles, srcDir, ossyDir)
819
924
  const clientHydrateInput = buildClientHydrateInput(pageFiles, srcDir, ossyDir)
820
925
 
821
926
  const { apiOverviewFiles } = resolveApiSource({
@@ -833,8 +938,6 @@ export const build = async (cliArgs) => {
833
938
  ? configPath
834
939
  : path.resolve(scriptDir, 'default-config.js')
835
940
 
836
- const pagesEntryPath = path.join(ossyDir, OSSY_PAGES_RUNTIME_BASENAME)
837
-
838
941
  const useDashboard = Object.keys(clientHydrateInput).length > 0
839
942
  const overviewSnap = getBuildOverviewSnapshot({
840
943
  pagesSourcePath: pagesGeneratedPath,
@@ -865,11 +968,6 @@ export const build = async (cliArgs) => {
865
968
  },
866
969
  })
867
970
  dashboard.start()
868
- if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
869
- dashboard.pushLog(
870
- `[resource-templates] merged ${resourceTemplatesResult.count} → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
871
- )
872
- }
873
971
  } else {
874
972
  console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild\x1b[0m')
875
973
  printBuildOverview({
@@ -878,28 +976,23 @@ export const build = async (cliArgs) => {
878
976
  configPath,
879
977
  pageFiles,
880
978
  })
881
- if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
882
- console.log(
883
- `[@ossy/app][resource-templates] merged ${resourceTemplatesResult.count} template(s) → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
884
- )
885
- }
886
979
  }
887
980
 
888
- const warnSink = dashboard
889
- ? (w) => {
890
- dashboard.pushLog(`rollup: ${w.message}`)
891
- }
892
- : undefined
981
+ if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
982
+ console.log(
983
+ `[@ossy/app][resource-templates] merged ${resourceTemplatesResult.count} template(s) ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
984
+ )
985
+ }
893
986
 
894
987
  const { taskOverviewFiles } = resolveTaskSource({ srcDir, buildPath })
895
988
  await compileOssyNodeArtifacts({
896
989
  pageFiles,
897
990
  srcDir,
898
991
  ossyDir,
992
+ buildPath,
899
993
  apiFiles: apiOverviewFiles,
900
994
  taskFiles: taskOverviewFiles,
901
995
  nodeEnv: 'production',
902
- onWarn: warnSink,
903
996
  })
904
997
 
905
998
  writeAppRuntimeShims({
@@ -918,8 +1011,6 @@ export const build = async (cliArgs) => {
918
1011
  copyPublicFrom: publicDir,
919
1012
  buildPath,
920
1013
  nodeEnv: 'production',
921
- pagesEntryPath,
922
- configSourcePath,
923
1014
  createClientRollupPlugins: createOssyClientRollupPlugins,
924
1015
  minifyBrowserStaticChunks,
925
1016
  reporter: dashboard,
@@ -929,8 +1020,5 @@ export const build = async (cliArgs) => {
929
1020
  }
930
1021
  }
931
1022
 
932
- if (useDashboard && dashboard) {
933
- console.log(' \x1b[2m────────────────────────────────────────\x1b[0m')
934
- }
935
1023
  console.log(' \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild finished\x1b[0m\n')
936
1024
  };
package/cli/index.js CHANGED
@@ -1,21 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { build } from './build.js'
3
- import { dev } from './dev.js'
4
3
 
5
4
  const [,, command, ...restArgs] = process.argv
6
5
 
7
6
  if (!command) {
8
7
  console.error(
9
- '[@ossy/app] No command provided. Usage: app dev | build'
8
+ '[@ossy/app] No command provided. Usage: app build'
10
9
  )
11
10
  process.exit(1)
12
11
  }
13
12
 
14
13
  const run = async () => {
15
- if (command === 'dev') {
16
- await dev(restArgs)
17
- return
18
- }
19
14
  if (command === 'build') {
20
15
  await build(restArgs)
21
16
  return
@@ -1,29 +1,8 @@
1
1
  import path from 'path'
2
- import url from 'url'
3
2
  import fs from 'fs'
4
3
  import { rollup } from 'rollup'
5
- import { BuildPage, buildPrerenderAppConfig } from './render-page.task.js'
6
4
  import { pageIdFromHydrateEntryName } from './build-terminal.js'
7
5
 
8
- /**
9
- * Maps an app route path to the file path express.static will serve for that URL
10
- * (`/` → `public/index.html`, `/a/b` → `public/a/b/index.html`).
11
- */
12
- export function staticHtmlPathForRoute (routePath, publicDir) {
13
- const p = typeof routePath === 'string' ? routePath : '/'
14
- if (p === '/' || p === '') {
15
- return path.join(publicDir, 'index.html')
16
- }
17
- const segments = p.replace(/^\//, '').split('/').filter(Boolean)
18
- return path.join(publicDir, ...segments, 'index.html')
19
- }
20
-
21
- function pathIsPrerenderable (routePath) {
22
- if (typeof routePath !== 'string') return false
23
- if (!routePath.startsWith('/') || routePath.includes(':')) return false
24
- return true
25
- }
26
-
27
6
  function copyPublicToBuild ({ copyPublicFrom, buildPath }) {
28
7
  if (!copyPublicFrom || !fs.existsSync(copyPublicFrom)) return
29
8
  const dest = path.join(buildPath, 'public')
@@ -31,10 +10,6 @@ function copyPublicToBuild ({ copyPublicFrom, buildPath }) {
31
10
  fs.cpSync(copyPublicFrom, dest, { recursive: true })
32
11
  }
33
12
 
34
- function countSettledFailures (results) {
35
- return results.filter((r) => r.status === 'rejected').length
36
- }
37
-
38
13
  async function bundleOneHydratePage ({
39
14
  entryName,
40
15
  stubPath,
@@ -113,156 +88,37 @@ async function bundleHydratePagesParallel ({
113
88
  })
114
89
  )
115
90
 
116
- const failures = countSettledFailures(results)
117
-
118
- return { results, failures }
119
- }
120
-
121
- async function prerenderOnePage ({
122
- route,
123
- buildTimeConfig,
124
- pageList,
125
- publicDir,
126
- }) {
127
- const appConfig = buildPrerenderAppConfig({
128
- buildTimeConfig,
129
- pageList,
130
- activeRouteId: route.id,
131
- urlPath: route.path,
132
- })
133
- const html = await BuildPage.handle({
134
- route,
135
- appConfig,
136
- isDevReloadEnabled: false,
137
- })
138
- const outPath = staticHtmlPathForRoute(route.path, publicDir)
139
- fs.mkdirSync(path.dirname(outPath), { recursive: true })
140
- fs.writeFileSync(outPath, html, 'utf8')
141
- }
142
-
143
- async function prerenderPagesParallel ({
144
- pagesEntryPath,
145
- configSourcePath,
146
- publicDir,
147
- reporter,
148
- }) {
149
- const cfgHref = url.pathToFileURL(path.resolve(configSourcePath)).href
150
- const pagesHref = url.pathToFileURL(path.resolve(pagesEntryPath)).href
151
-
152
- const configModule = await import(cfgHref)
153
- const pagesModule = await import(pagesHref)
154
-
155
- const buildTimeConfig = configModule?.default ?? configModule ?? {}
156
- const pageList = pagesModule?.default ?? []
157
-
158
- const routesToRender = []
159
- for (const route of pageList) {
160
- if (typeof route?.module !== 'string' || !route.module) continue
161
- if (!pathIsPrerenderable(route.path)) {
162
- reporter?.skipPrerender?.(
163
- route.id,
164
- `not static (${JSON.stringify(route.path)})`
165
- )
166
- console.warn(
167
- `[@ossy/app][prerender] Skipping "${route.id}" (path not prerenderable: ${JSON.stringify(route.path)})`
168
- )
169
- continue
170
- }
171
- routesToRender.push(route)
172
- }
173
-
174
- const results = await Promise.allSettled(
175
- routesToRender.map(async (route) => {
176
- const t0 = Date.now()
177
- reporter?.startPrerender?.(route.id)
178
- try {
179
- await prerenderOnePage({
180
- route,
181
- buildTimeConfig,
182
- pageList,
183
- publicDir,
184
- })
185
- reporter?.completePrerender?.(route.id, { ok: true, ms: Date.now() - t0 })
186
- } catch (error) {
187
- reporter?.completePrerender?.(route.id, {
188
- ok: false,
189
- ms: Date.now() - t0,
190
- error,
191
- })
192
- console.error(
193
- `[@ossy/app][prerender] "${route?.id}" (${route?.path}) failed:`,
194
- error
195
- )
196
- throw error
197
- }
198
- })
199
- )
200
- const failures = countSettledFailures(results)
201
-
91
+ const failures = results.filter((r) => r.status === 'rejected').length
202
92
  return { results, failures }
203
93
  }
204
94
 
205
- async function runProduction ({
206
- clientHydrateInput,
207
- pageFilesLength,
208
- copyPublicFrom,
209
- buildPath,
210
- nodeEnv,
211
- pagesEntryPath,
212
- configSourcePath,
213
- createClientRollupPlugins,
214
- minifyBrowserStaticChunks,
215
- reporter,
216
- }) {
217
- if (pageFilesLength === 0 || Object.keys(clientHydrateInput).length === 0) {
218
- return { bundleFailures: 0, prerenderFailures: 0 }
219
- }
220
-
221
- const { failures: bundleFailures } = await bundleHydratePagesParallel({
222
- clientHydrateInput,
223
- buildPath,
224
- copyPublicFrom,
225
- nodeEnv,
226
- buildPathForPlugins: buildPath,
227
- createClientRollupPlugins,
228
- minifyBrowserStaticChunks,
229
- reporter,
230
- })
231
-
232
- const { failures: prerenderFailures } = await prerenderPagesParallel({
233
- pagesEntryPath,
234
- configSourcePath,
235
- publicDir: path.join(buildPath, 'public'),
236
- reporter,
237
- })
238
-
239
- if (bundleFailures + prerenderFailures > 0) {
240
- console.warn(
241
- `[@ossy/app][build] Finished with ${bundleFailures} client bundle error(s) and ${prerenderFailures} prerender error(s)`
242
- )
243
- }
244
-
245
- return { bundleFailures, prerenderFailures }
246
- }
247
-
248
95
  export default {
249
96
  type: '@ossy/app/prerender-react',
250
- /** `input.op`: `runProduction` | `prerenderPagesParallel` (see callers in `build.js` / `dev.js`). */
251
97
  async handler (input) {
252
98
  const op = input?.op
253
99
  if (op === 'runProduction') {
254
- return runProduction(input)
255
- }
256
- if (op === 'prerenderPagesParallel') {
257
- return prerenderPagesParallel({
258
- pagesEntryPath: input.pagesEntryPath,
259
- configSourcePath: input.configSourcePath,
260
- publicDir: input.publicDir,
261
- reporter: input.reporter,
100
+ const { clientHydrateInput, pageFilesLength, copyPublicFrom, buildPath, nodeEnv,
101
+ buildPathForPlugins, createClientRollupPlugins, minifyBrowserStaticChunks, reporter } = input
102
+ if (pageFilesLength === 0 || Object.keys(clientHydrateInput).length === 0) {
103
+ return { bundleFailures: 0 }
104
+ }
105
+ const { failures: bundleFailures } = await bundleHydratePagesParallel({
106
+ clientHydrateInput,
107
+ buildPath,
108
+ copyPublicFrom,
109
+ nodeEnv,
110
+ buildPathForPlugins: buildPathForPlugins ?? buildPath,
111
+ createClientRollupPlugins,
112
+ minifyBrowserStaticChunks,
113
+ reporter,
262
114
  })
115
+ if (bundleFailures > 0) {
116
+ console.warn(`[@ossy/app][build] Finished with ${bundleFailures} client bundle error(s)`)
117
+ }
118
+ return { bundleFailures }
263
119
  }
264
120
  throw new Error(
265
- `[@ossy/app][prerender-react] Unknown op: ${String(op)} (expected runProduction | prerenderPagesParallel)`
121
+ `[@ossy/app][prerender-react] Unknown op: ${String(op)} (expected runProduction)`
266
122
  )
267
123
  },
268
124
  }
@@ -1,30 +1,22 @@
1
1
  import path from 'node:path'
2
2
  import { fileURLToPath, pathToFileURL } from 'node:url'
3
- import React, { createElement } from 'react'
4
- import { prerenderToNodeStream } from 'react-dom/static'
5
3
 
6
4
  const __ossyDir = path.dirname(fileURLToPath(import.meta.url))
7
5
 
8
- async function loadPageDefaultExport (route) {
9
- if (typeof route?.module !== 'string' || !route.module) {
10
- throw new Error(
11
- `[@ossy/app][BuildPage] Route "${route?.id ?? '?'}" has no compiled module path (re-run build so pages.generated.json includes "module").`
12
- )
6
+ async function loadSsrBundle (route) {
7
+ if (typeof route?.id !== 'string' || !route.id) {
8
+ throw new Error(`[@ossy/app][BuildPage] Route has no id`)
13
9
  }
14
- const abs = path.join(__ossyDir, route.module)
15
- const mod = await import(pathToFileURL(abs).href)
16
- const def = mod?.default
17
- if (typeof def !== 'function') {
10
+ const bundlePath = path.join(__ossyDir, '..', 'ssr', `${route.id}.mjs`)
11
+ const mod = await import(pathToFileURL(bundlePath).href)
12
+ if (typeof mod?.renderPage !== 'function') {
18
13
  throw new Error(
19
- `[@ossy/app][BuildPage] Page "${route?.id}" must export default as a function component (got ${typeof def}).`
14
+ `[@ossy/app][BuildPage] SSR bundle for "${route.id}" must export renderPage (got ${typeof mod?.renderPage}).`
20
15
  )
21
16
  }
22
- return def
17
+ return mod
23
18
  }
24
19
 
25
- /**
26
- * App shell config for SSR / prerender (mirrors client: theme, pages metadata, props for the active page).
27
- */
28
20
  export function buildPrerenderAppConfig ({
29
21
  buildTimeConfig,
30
22
  pageList,
@@ -32,11 +24,9 @@ export function buildPrerenderAppConfig ({
32
24
  urlPath,
33
25
  isAuthenticated = false,
34
26
  }) {
35
- /** `module` is the compiled page path under `.ossy/` (Node `import()` only; not loaded as raw ESM in the browser). */
36
27
  const pages = pageList.map((page) => ({
37
28
  id: page?.id,
38
29
  path: page?.path,
39
- ...(typeof page?.module === 'string' ? { module: page.module } : {}),
40
30
  }))
41
31
  return {
42
32
  ...buildTimeConfig,
@@ -50,23 +40,8 @@ export function buildPrerenderAppConfig ({
50
40
  }
51
41
  }
52
42
 
53
- /** Strips non-JSON content for the bootstrap script; keeps serializable route fields including `module`. */
54
- export function appConfigForBootstrap (appConfig) {
55
- if (!appConfig || typeof appConfig !== 'object') return appConfig
56
- const pages = Array.isArray(appConfig.pages)
57
- ? appConfig.pages.map(({ id, path, module }) => ({
58
- id,
59
- path,
60
- ...(typeof module === 'string' ? { module } : {}),
61
- }))
62
- : appConfig.pages
63
- return { ...appConfig, pages }
64
- }
65
-
66
- /** Plain data clone so server render props match `JSON.parse(JSON.stringify(...))` on the client. */
67
43
  function jsonSafeClone (value) {
68
44
  if (value == null || typeof value !== 'object') return value
69
- if (typeof value === 'function' || React.isValidElement(value)) return value
70
45
  try {
71
46
  return JSON.parse(JSON.stringify(value))
72
47
  } catch {
@@ -74,42 +49,24 @@ function jsonSafeClone (value) {
74
49
  }
75
50
  }
76
51
 
77
- /**
78
- * Props passed to `<App>` for SSR and the exact object embedded in `window.__INITIAL_APP_CONFIG__`.
79
- * Keeps server and hydrate trees aligned (fixes React #418 hydration mismatches).
80
- */
81
52
  export function buildHydrationAppConfig (appConfig) {
82
- const base = appConfigForBootstrap(appConfig)
53
+ if (!appConfig || typeof appConfig !== 'object') return appConfig
83
54
  return {
84
- ...base,
85
- theme: jsonSafeClone(base.theme),
86
- themes: jsonSafeClone(base.themes),
87
- resourceTemplates: jsonSafeClone(base.resourceTemplates),
55
+ ...appConfig,
56
+ theme: jsonSafeClone(appConfig.theme),
57
+ themes: jsonSafeClone(appConfig.themes),
58
+ resourceTemplates: jsonSafeClone(appConfig.resourceTemplates),
88
59
  }
89
60
  }
90
61
 
91
62
  export const BuildPage = {
92
- async handle ({ route, appConfig, isDevReloadEnabled }) {
63
+ async handle ({ route, appConfig }) {
93
64
  const hydrationConfig = buildHydrationAppConfig(appConfig)
94
- const Page = await loadPageDefaultExport(route)
95
- const rootElement = createElement(Page, hydrationConfig)
96
- const devReloadScript = isDevReloadEnabled
97
- ? `(function(){try{var es=new EventSource('/__ossy_reload');es.addEventListener('reload',function(){location.reload();});}catch(e){}})();`
98
- : ``
99
-
100
- const hydrateUrl = `/static/${route.id}.js`
101
- const { prelude } = await prerenderToNodeStream(rootElement, {
102
- bootstrapScriptContent: `window.__INITIAL_APP_CONFIG__ = ${JSON.stringify(hydrationConfig)};${devReloadScript}`,
103
- bootstrapModules: [hydrateUrl],
104
- })
65
+ const { renderPage } = await loadSsrBundle(route)
105
66
 
106
- return new Promise((resolve, reject) => {
107
- let data = ''
108
- prelude.on('data', (chunk) => {
109
- data += chunk
110
- })
111
- prelude.on('end', () => resolve(data))
112
- prelude.on('error', reject)
67
+ return renderPage(hydrationConfig, {
68
+ bootstrapScriptContent: `window.__INITIAL_APP_CONFIG__=${JSON.stringify(hydrationConfig)}`,
69
+ bootstrapModules: [`/static/${route.id}.js`],
113
70
  })
114
71
  },
115
72
  }
package/cli/server.js CHANGED
@@ -39,9 +39,6 @@ const app = express();
39
39
  const currentDir = path.dirname(url.fileURLToPath(import.meta.url))
40
40
  const ROOT_PATH = path.resolve(currentDir, 'public')
41
41
 
42
- const isDevReloadEnabled = process.env.OSSY_DEV_RELOAD === '1'
43
- const reloadClients = new Set()
44
-
45
42
  function parsePortFromArgv(argv) {
46
43
  // Supports: --port 4000, --port=4000, -p 4000
47
44
  const idx = argv.findIndex(a => a === '--port' || a === '-p')
@@ -67,33 +64,6 @@ if (Middleware !== undefined) {
67
64
  console.log(`[@ossy/app][server] ${Middleware?.length || 0} custom middleware loaded`)
68
65
  }
69
66
 
70
- if (isDevReloadEnabled) {
71
- app.get('/__ossy_reload', (req, res) => {
72
- res.status(200)
73
- res.setHeader('Content-Type', 'text/event-stream')
74
- res.setHeader('Cache-Control', 'no-cache')
75
- res.setHeader('Connection', 'keep-alive')
76
- res.flushHeaders?.()
77
-
78
- res.write('event: connected\ndata: ok\n\n')
79
- reloadClients.add(res)
80
-
81
- req.on('close', () => {
82
- reloadClients.delete(res)
83
- })
84
- })
85
-
86
- app.post('/__ossy_reload', (req, res) => {
87
- for (const client of reloadClients) {
88
- try {
89
- client.write('event: reload\ndata: now\n\n')
90
- } catch {
91
- // ignore broken connections
92
- }
93
- }
94
- res.status(204).end()
95
- })
96
- }
97
67
 
98
68
  const middleware = [
99
69
  morgan('tiny'),
@@ -163,11 +133,7 @@ app.all('*all', async (req, res) => {
163
133
  urlPath: requestUrl,
164
134
  isAuthenticated: !!req.isAuthenticated,
165
135
  })
166
- const html = await BuildPage.handle({
167
- route: pageRoute,
168
- appConfig,
169
- isDevReloadEnabled,
170
- })
136
+ const html = await BuildPage.handle({ route: pageRoute, appConfig })
171
137
  res.status(200).type('html').send(html)
172
138
  return
173
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "1.11.7",
3
+ "version": "1.11.8",
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.11.7",
31
- "@ossy/design-system": "^1.11.7",
32
- "@ossy/pages": "^1.11.7",
33
- "@ossy/router": "^1.11.7",
34
- "@ossy/router-react": "^1.11.7",
35
- "@ossy/sdk": "^1.11.7",
36
- "@ossy/sdk-react": "^1.11.7",
37
- "@ossy/themes": "^1.11.7",
30
+ "@ossy/connected-components": "^1.11.8",
31
+ "@ossy/design-system": "^1.11.8",
32
+ "@ossy/pages": "^1.11.8",
33
+ "@ossy/router": "^1.11.8",
34
+ "@ossy/router-react": "^1.11.8",
35
+ "@ossy/sdk": "^1.11.8",
36
+ "@ossy/sdk-react": "^1.11.8",
37
+ "@ossy/themes": "^1.11.8",
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": "43d1eca8557efc9586398a893ce9832b2ebf0444"
70
+ "gitHead": "16cef74bf4a2709ec3bf91cb73f2149703086b62"
71
71
  }
package/cli/dev.js DELETED
@@ -1,342 +0,0 @@
1
- import path from 'path';
2
- import url from 'url';
3
- import fs from 'fs';
4
- import {
5
- printBuildOverview,
6
- getBuildOverviewSnapshot,
7
- filePathToRoute,
8
- discoverFilesByPattern,
9
- PAGE_FILE_PATTERN,
10
- writePagesManifest,
11
- resolveApiSource,
12
- resolveTaskSource,
13
- resetOssyBuildDir,
14
- compileOssyNodeArtifacts,
15
- copyOssyAppRuntime,
16
- writeAppRuntimeShims,
17
- createOssyAppBundlePlugins,
18
- createOssyClientRollupPlugins,
19
- writePageHydrateStubs,
20
- buildClientHydrateInput,
21
- clientHydrateIdForPage,
22
- ossyGeneratedDir,
23
- OSSY_GEN_PAGES_BASENAME,
24
- OSSY_PAGES_RUNTIME_BASENAME,
25
- writeResourceTemplatesBarrelIfPresent,
26
- resourceTemplatesDir,
27
- OSSY_RESOURCE_TEMPLATES_OUT,
28
- } from './build.js';
29
- import prerenderReactTask from './prerender-react.task.js'
30
- import { createBuildDashboard } from './build-terminal.js'
31
- import { watch } from 'rollup';
32
- import arg from 'arg'
33
- import { spawn } from 'node:child_process'
34
- // import inject from '@rollup/plugin-inject'
35
-
36
- export const dev = async (cliArgs) => {
37
- const options = arg({
38
- '--config': String,
39
- '-c': '--config',
40
- }, { argv: cliArgs, permissive: true })
41
-
42
-
43
- const scriptDir = path.dirname(url.fileURLToPath(import.meta.url))
44
- const buildPath = path.resolve('build')
45
- const srcDir = path.resolve('src')
46
- const configPath = path.resolve(options['--config'] || 'src/config.js');
47
- let currentPageFiles = discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN)
48
-
49
- resetOssyBuildDir(buildPath)
50
-
51
- const resourceTemplatesResult = writeResourceTemplatesBarrelIfPresent({
52
- cwd: process.cwd(),
53
- log: false,
54
- })
55
- let resourceTemplatesDevLogged = false
56
-
57
- const ossyDir = ossyGeneratedDir(buildPath)
58
- const pagesGeneratedPath = path.join(ossyDir, OSSY_GEN_PAGES_BASENAME)
59
- writePagesManifest({
60
- pageFiles: currentPageFiles,
61
- srcDir,
62
- pagesGeneratedPath,
63
- })
64
- writePageHydrateStubs(currentPageFiles, srcDir, ossyDir)
65
- const clientHydrateInput = buildClientHydrateInput(currentPageFiles, srcDir, ossyDir)
66
-
67
- let apiOverviewFiles = []
68
- let taskOverviewFiles = []
69
- const refreshApiTaskManifests = () => {
70
- apiOverviewFiles = resolveApiSource({ srcDir, buildPath }).apiOverviewFiles
71
- taskOverviewFiles = resolveTaskSource({ srcDir, buildPath }).taskOverviewFiles
72
- }
73
- refreshApiTaskManifests()
74
- let middlewareSourcePath = path.resolve(options['--middleware-source'] || 'src/middleware.js');
75
- const publicDir = path.resolve('public')
76
-
77
- if (currentPageFiles.length === 0) {
78
- console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mdev\x1b[0m')
79
- printBuildOverview({
80
- pagesSourcePath: pagesGeneratedPath,
81
- apiOverviewFiles,
82
- configPath,
83
- pageFiles: currentPageFiles,
84
- })
85
- if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
86
- console.log(
87
- `[@ossy/app][resource-templates] merged ${resourceTemplatesResult.count} template(s) → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
88
- )
89
- }
90
- }
91
-
92
- if (!fs.existsSync(middlewareSourcePath)) {
93
- middlewareSourcePath = path.resolve(scriptDir, 'middleware.js')
94
- }
95
-
96
- const configSourcePath = fs.existsSync(configPath)
97
- ? configPath
98
- : path.resolve(scriptDir, 'default-config.js')
99
-
100
- const pagesEntryPath = path.join(ossyDir, OSSY_PAGES_RUNTIME_BASENAME)
101
-
102
- const runNodeBundles = async () => {
103
- refreshApiTaskManifests()
104
- await compileOssyNodeArtifacts({
105
- pageFiles: currentPageFiles,
106
- srcDir,
107
- ossyDir,
108
- apiFiles: apiOverviewFiles,
109
- taskFiles: taskOverviewFiles,
110
- nodeEnv: 'development',
111
- })
112
- writeAppRuntimeShims({
113
- middlewareSourcePath,
114
- configSourcePath,
115
- ossyDir,
116
- })
117
- copyOssyAppRuntime({ scriptDir, buildPath })
118
- }
119
-
120
- await runNodeBundles()
121
-
122
- const clientPlugins = createOssyClientRollupPlugins({
123
- nodeEnv: 'development',
124
- copyPublicFrom: publicDir,
125
- buildPath,
126
- })
127
-
128
- // `dir` must not be an ancestor of hydrate stubs (`build/.ossy/…`) or Rollup watch errors.
129
- const clientOutput = {
130
- dir: path.join(buildPath, 'public'),
131
- format: 'esm',
132
- entryFileNames ({ name }) {
133
- if (name.startsWith('hydrate__')) {
134
- const pageId = name.slice('hydrate__'.length)
135
- return `static/${pageId}.js`
136
- }
137
- return 'static/[name].js'
138
- },
139
- chunkFileNames: 'static/[name]-[hash].js',
140
- }
141
-
142
- let restartTimer = null
143
- const scheduleRestart = () => {
144
- clearTimeout(restartTimer)
145
- restartTimer = setTimeout(async () => {
146
- await triggerReload()
147
- restartServer()
148
- }, 100)
149
- }
150
-
151
- let serverProcess = null
152
- const startServer = () => {
153
- if (serverProcess) return
154
- serverProcess = spawn(process.execPath, [path.resolve(buildPath, 'server.js'), ...process.argv.slice(3)], {
155
- stdio: 'inherit',
156
- env: {
157
- ...process.env,
158
- OSSY_DEV_RELOAD: '1',
159
- NODE_ENV: 'development',
160
- },
161
- })
162
- serverProcess.on('exit', () => {
163
- serverProcess = null
164
- })
165
- }
166
-
167
- const restartServer = () => {
168
- if (!serverProcess) return startServer()
169
- serverProcess.kill('SIGTERM')
170
- serverProcess = null
171
- startServer()
172
- }
173
-
174
- const triggerReload = async () => {
175
- const port = process.env.PORT || '3000'
176
- try {
177
- await fetch(`http://localhost:${port}/__ossy_reload`, { method: 'POST' })
178
- } catch {
179
- // server might not be up yet
180
- }
181
- }
182
-
183
- const nodeWatchOpts = { watch: { clearScreen: false } }
184
- const watchConfigs = []
185
- if (Object.keys(clientHydrateInput).length > 0) {
186
- watchConfigs.push({
187
- input: clientHydrateInput,
188
- output: clientOutput,
189
- plugins: clientPlugins,
190
- watch: { clearScreen: false },
191
- })
192
- } else {
193
- watchConfigs.push({
194
- input: '\0ossy-dev-noop',
195
- output: {
196
- file: path.join(ossyDir, '.dev-noop-out.mjs'),
197
- format: 'esm',
198
- inlineDynamicImports: true,
199
- },
200
- plugins: [
201
- {
202
- name: 'ossy-dev-noop',
203
- resolveId (id) {
204
- if (id === '\0ossy-dev-noop') return id
205
- return null
206
- },
207
- load (id) {
208
- if (id === '\0ossy-dev-noop') return 'export default 0\n'
209
- return null
210
- },
211
- },
212
- ...createOssyAppBundlePlugins({ nodeEnv: 'development' }),
213
- ],
214
- ...nodeWatchOpts,
215
- })
216
- }
217
- const watcher = watch(watchConfigs)
218
-
219
- watcher.on('event', async (event) => {
220
- if (event.code === 'BUNDLE_START') {
221
- console.log(' \x1b[2m◐\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mrollup watch …\x1b[0m')
222
- }
223
- if (event.code === 'ERROR') {
224
- console.error(' \x1b[31m✖\x1b[0m \x1b[1m@ossy/app\x1b[0m build error', event.error)
225
- }
226
- if (event.code === 'BUNDLE_END') {
227
- console.log(
228
- ` \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbundles\x1b[0m \x1b[2m(${event.duration}ms)\x1b[0m`
229
- )
230
- }
231
- if (event.code === 'END') {
232
- writeAppRuntimeShims({
233
- middlewareSourcePath,
234
- configSourcePath,
235
- ossyDir,
236
- })
237
- copyOssyAppRuntime({ scriptDir, buildPath })
238
- if (currentPageFiles.length > 0) {
239
- const pageIds = [
240
- ...new Set(currentPageFiles.map((f) => clientHydrateIdForPage(f, srcDir))),
241
- ].sort()
242
- const overviewSnap = getBuildOverviewSnapshot({
243
- pagesSourcePath: pagesGeneratedPath,
244
- apiOverviewFiles,
245
- configPath,
246
- pageFiles: currentPageFiles,
247
- })
248
- const idToPath = Object.fromEntries(
249
- currentPageFiles.map((f) => [
250
- clientHydrateIdForPage(f, srcDir),
251
- filePathToRoute(f, srcDir).path,
252
- ])
253
- )
254
- const reporter = pageIds.length
255
- ? createBuildDashboard({
256
- mode: 'prerender-only',
257
- pageIds,
258
- idToPath,
259
- overview: {
260
- title: '@ossy/app dev',
261
- configRel: overviewSnap.configRel,
262
- apiRoutes: overviewSnap.apiRoutes,
263
- },
264
- })
265
- : null
266
- reporter?.start()
267
- if (
268
- !resourceTemplatesDevLogged &&
269
- resourceTemplatesResult.wrote &&
270
- resourceTemplatesResult.path &&
271
- reporter
272
- ) {
273
- reporter.pushLog(
274
- `[resource-templates] merged ${resourceTemplatesResult.count} → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
275
- )
276
- resourceTemplatesDevLogged = true
277
- }
278
- try {
279
- await prerenderReactTask.handler({
280
- op: 'prerenderPagesParallel',
281
- pagesEntryPath,
282
- configSourcePath,
283
- publicDir: path.join(buildPath, 'public'),
284
- reporter,
285
- })
286
- } catch (err) {
287
- console.error(' \x1b[31m✖\x1b[0m \x1b[1m@ossy/app\x1b[0m prerender failed', err)
288
- } finally {
289
- reporter?.dispose()
290
- }
291
- }
292
- scheduleRestart()
293
- }
294
- })
295
-
296
- const regenApiGenerated = () => {
297
- if (options['--api-source']) return
298
- void runNodeBundles().then(() => scheduleRestart())
299
- }
300
-
301
- const regenTasksGenerated = () => {
302
- void runNodeBundles().then(() => scheduleRestart())
303
- }
304
-
305
- fs.watch(srcDir, { recursive: true }, (eventType, filename) => {
306
- if (!filename) return
307
- if (/\.page\.(jsx?|tsx?)$/.test(filename)) {
308
- const refreshedPageFiles = discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN)
309
- currentPageFiles = refreshedPageFiles
310
- const regenPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_PAGES_BASENAME)
311
- writePagesManifest({
312
- pageFiles: refreshedPageFiles,
313
- srcDir,
314
- pagesGeneratedPath: regenPath,
315
- })
316
- writePageHydrateStubs(refreshedPageFiles, srcDir, ossyDir)
317
- void runNodeBundles().then(() => {
318
- if (typeof watcher?.invalidate === 'function') {
319
- for (const f of refreshedPageFiles) {
320
- const hid = clientHydrateIdForPage(f, srcDir)
321
- watcher.invalidate(path.join(ossyDir, `hydrate-${hid}.jsx`))
322
- }
323
- }
324
- scheduleRestart()
325
- })
326
- }
327
- if (/\.api\.(mjs|cjs|js)$/.test(filename)) {
328
- regenApiGenerated()
329
- }
330
- if (/\.task\.(mjs|cjs|js)$/.test(filename)) {
331
- regenTasksGenerated()
332
- }
333
- const norm = filename.replace(/\\/g, '/')
334
- if (/\.resource\.js$/.test(norm) && norm.includes('resource-templates/')) {
335
- writeResourceTemplatesBarrelIfPresent({ cwd: process.cwd(), log: false })
336
- const rtOut = path.join(resourceTemplatesDir(process.cwd()), OSSY_RESOURCE_TEMPLATES_OUT)
337
- if (fs.existsSync(rtOut) && typeof watcher?.invalidate === 'function') {
338
- watcher.invalidate(rtOut)
339
- }
340
- }
341
- })
342
- };