@ossy/app 1.11.6 → 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.
package/README.md CHANGED
@@ -21,9 +21,9 @@ export const metadata = { path: { en: '/about', sv: '/om' } }
21
21
  export default () => <h1>About</h1>
22
22
  ```
23
23
 
24
- During `dev` / `build`, the tooling writes **JSON manifests** under **`build/.ossy/`**: **`pages.generated.json`** (after compile: route ids, paths, `sourceFile`, merged `metadata`, and **`module`** — a `page-modules/<id>.mjs` path used for lazy `import()` in the browser and on SSR), **`pages.bundle.json`** (compiled module index), plus the same pattern for API and tasks. Small **`*.runtime.mjs`** loaders (copied from `@ossy/app`) load the app shell; **`pages.runtime.mjs`** exports the route table from **`pages.generated.json`** only. The server exposes compiled page modules at **`/__ossy/page-modules/`** for same-origin dynamic imports.
24
+ During `dev` / `build`, the tooling writes **JSON manifests** under **`build/.ossy/`**: **`pages.generated.json`** (after compile: route ids, paths, `sourceFile`, merged `metadata`, and **`module`** — a `page-modules/<id>.mjs` path the **Node** server `import()`s per request for SSR), **`pages.bundle.json`** (compiled module index), plus the same pattern for API and tasks. **`pages.runtime.mjs`** exports the route table from **`pages.generated.json`** only.
25
25
 
26
- **Client JS (per-page):** For each `*.page.jsx`, the build emits **`build/.ossy/hydrate-<pageId>.jsx`** → **`public/static/<pageId>.js`**. The HTML for a request only loads the hydrate script for the **current** route (full document navigation), so other pages’ components are not part of that entry. React and shared dependencies still go into hashed shared chunks. The inline config (`window.__INITIAL_APP_CONFIG__`) no longer includes the full `pages` list—only request-time fields (theme, `apiUrl`, etc.).
26
+ **Client JS (per-page):** For each `*.page.jsx`, the build emits **`build/.ossy/hydrate-<pageId>.jsx`** → **`public/static/<pageId>.js`**. Rollup bundles that entry with the page source so **`react` resolves in the browser** (unlike the Node `page-modules/*.mjs` chunks, which keep `react` external). The HTML only loads the hydrate script for the **current** route. The inline config (`window.__INITIAL_APP_CONFIG__`) keeps request-time fields (theme, `apiUrl`, etc.); `pages` in config include `id`, `path`, and `module` for consistency with the manifest.
27
27
 
28
28
  Add `src/config.js` for workspace and theme:
29
29
 
@@ -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
@@ -69,8 +69,6 @@ export const OSSY_TASKS_RUNTIME_BASENAME = 'tasks.runtime.mjs'
69
69
 
70
70
  export const OSSY_PAGE_MODULES_DIRNAME = 'page-modules'
71
71
 
72
- /** Express serves compiled page modules here for browser `import()`. Must match `server.js`. */
73
- export const OSSY_PAGE_MODULE_WEB_PREFIX = '/__ossy/page-modules'
74
72
  /** Tiny Rollup inputs that re-export `metadata` so per-page server bundles keep i18n paths. */
75
73
  export const OSSY_PAGE_SERVER_ENTRIES_DIRNAME = 'page-server-entries'
76
74
  export const OSSY_API_MODULES_DIRNAME = 'api-modules'
@@ -89,6 +87,11 @@ export const OSSY_PAGE_SERVER_EXTERNAL = [
89
87
  'react/jsx-runtime',
90
88
  ]
91
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
+
92
95
  /** Per-page client entries: `hydrate-<pageId>.jsx` under `.ossy/` */
93
96
  const HYDRATE_STUB_PREFIX = 'hydrate-'
94
97
  const HYDRATE_STUB_SUFFIX = '.jsx'
@@ -202,7 +205,8 @@ export function createOssyAppBundlePlugins ({ nodeEnv }) {
202
205
  }
203
206
 
204
207
  /**
205
- * 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.
206
210
  */
207
211
  export function createOssyClientRollupPlugins ({ nodeEnv, copyPublicFrom, buildPath }) {
208
212
  const plugins = [
@@ -214,7 +218,7 @@ export function createOssyClientRollupPlugins ({ nodeEnv, copyPublicFrom, buildP
214
218
  nodeExternals({
215
219
  deps: false,
216
220
  devDeps: true,
217
- peerDeps: true,
221
+ peerDeps: false,
218
222
  packagePath: path.join(process.cwd(), 'package.json'),
219
223
  }),
220
224
  resolveCommonJsDependencies(),
@@ -235,6 +239,106 @@ export function createOssyClientRollupPlugins ({ nodeEnv, copyPublicFrom, buildP
235
239
  return plugins
236
240
  }
237
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
+
238
342
  /** Bundles a single Node ESM file (inline dynamic imports) for SSR / API / tasks. */
239
343
  export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv, onWarn, external }) {
240
344
  const bundle = await rollup({
@@ -455,41 +559,42 @@ export async function compileTaskServerModules ({ taskFiles, ossyDir, nodeEnv, o
455
559
 
456
560
  /**
457
561
  * Merges compiled `metadata` + `module` into `pages.generated.json` (same order as `pageBundleList`).
458
- * Enables lazy `import()` in the browser and Node without putting `Component` on the route object.
562
+ * Writes `module` on each route for Node SSR (`import()` of `page-modules/*.mjs`); hydrate uses a separate Rollup client entry.
459
563
  */
460
- export async function enrichPagesGeneratedManifest ({
461
- ossyDir,
462
- pagesGeneratedPath,
463
- pageBundleList,
464
- }) {
465
- 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
466
570
  const raw = JSON.parse(fs.readFileSync(pagesGeneratedPath, 'utf8'))
467
571
  const basePages = raw?.pages
468
- if (!Array.isArray(basePages) || basePages.length !== pageBundleList.length) {
469
- throw new Error(
470
- '[@ossy/app] pages.generated.json page count must match compiled page modules (re-run build).'
471
- )
472
- }
572
+ if (!Array.isArray(basePages) || basePages.length === 0) return
573
+
473
574
  const pages = []
474
- for (let i = 0; i < basePages.length; i++) {
475
- const abs = path.join(ossyDir, pageBundleList[i].module)
476
- const mod = await import(pathToFileURL(abs).href)
477
- const meta = mod?.metadata && typeof mod.metadata === 'object' ? mod.metadata : {}
478
- 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
+ }
479
587
  const merged = {
480
- ...derived,
588
+ id: basePage.id,
589
+ path: basePage.path,
481
590
  ...meta,
482
- sourceFile: basePages[i].sourceFile,
483
- module: pageBundleList[i].module,
591
+ sourceFile: basePage.sourceFile,
592
+ module: moduleRelPath,
484
593
  }
485
594
  try {
486
595
  JSON.stringify(merged)
487
596
  } catch {
488
- pages.push({
489
- ...derived,
490
- sourceFile: basePages[i].sourceFile,
491
- module: pageBundleList[i].module,
492
- })
597
+ pages.push({ id: basePage.id, path: basePage.path, sourceFile: basePage.sourceFile, module: moduleRelPath })
493
598
  continue
494
599
  }
495
600
  pages.push(merged)
@@ -498,26 +603,23 @@ export async function enrichPagesGeneratedManifest ({
498
603
  }
499
604
 
500
605
  /**
501
- * 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.
502
607
  */
503
608
  export async function compileOssyNodeArtifacts ({
504
609
  pageFiles,
505
610
  srcDir,
506
611
  ossyDir,
612
+ buildPath,
507
613
  apiFiles,
508
614
  taskFiles,
509
615
  nodeEnv,
510
616
  onWarn,
511
617
  }) {
512
- const [pageBundleList, apiModuleList, taskModuleList] = await Promise.all([
513
- compilePageServerModules({ pageFiles, srcDir, ossyDir, nodeEnv, onWarn }),
618
+ const [, apiModuleList, taskModuleList] = await Promise.all([
619
+ compilePageSsrModules({ pageFiles, srcDir, ossyDir, buildPath, nodeEnv, onWarn }),
514
620
  compileApiServerModules({ apiFiles, ossyDir, nodeEnv, onWarn }),
515
621
  compileTaskServerModules({ taskFiles, ossyDir, nodeEnv, onWarn }),
516
622
  ])
517
- writeOssyJson(path.join(ossyDir, OSSY_PAGES_BUNDLE_BASENAME), {
518
- version: 1,
519
- pages: pageBundleList,
520
- })
521
623
  writeOssyJson(path.join(ossyDir, OSSY_API_BUNDLE_BASENAME), {
522
624
  version: 1,
523
625
  modules: apiModuleList,
@@ -529,7 +631,6 @@ export async function compileOssyNodeArtifacts ({
529
631
  await enrichPagesGeneratedManifest({
530
632
  ossyDir,
531
633
  pagesGeneratedPath: path.join(ossyDir, OSSY_GEN_PAGES_BASENAME),
532
- pageBundleList,
533
634
  })
534
635
  }
535
636
 
@@ -561,7 +662,7 @@ export function clientHydrateIdForPage (pageAbsPath, srcDir) {
561
662
  return idMatch ? idMatch[1] : derived.id
562
663
  }
563
664
 
564
- /** Posix path relative to `build/.ossy/` for the compiled server/browser page module. */
665
+ /** Posix path relative to `build/.ossy/` for the compiled **Node** page module (SSR). */
565
666
  export function pageServerModuleRelPath (pageAbsPath, srcDir) {
566
667
  const pageId = clientHydrateIdForPage(pageAbsPath, srcDir)
567
668
  const safeId = String(pageId).replace(/[^a-zA-Z0-9_-]+/g, '-') || 'page'
@@ -600,35 +701,27 @@ export function writePageServerRollupEntry ({ pageAbsPath, stubPath }) {
600
701
  }
601
702
 
602
703
  /**
603
- * One client entry per page: `import()` the compiled page module URL, then hydrates the document.
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.
604
707
  */
605
708
  export function generatePageHydrateModule ({ pageAbsPath, stubAbsPath, srcDir }) {
606
- void stubAbsPath
607
- const outName = path.posix.basename(pageServerModuleRelPath(pageAbsPath, srcDir))
608
- const pageImportUrl = `${OSSY_PAGE_MODULE_WEB_PREFIX}/${outName}`
609
- const pageImportLiteral = JSON.stringify(pageImportUrl)
709
+ const rel = relToGeneratedImport(stubAbsPath, pageAbsPath)
610
710
  return [
611
711
  '// Generated by @ossy/app — do not edit',
612
712
  '',
613
713
  "import React, { createElement } from 'react'",
614
- "import 'react-dom'",
615
714
  "import { hydrateRoot } from 'react-dom/client'",
715
+ `import * as _page from './${rel}'`,
616
716
  '',
617
- 'async function main () {',
618
- ' const initialConfig = window.__INITIAL_APP_CONFIG__ || {}',
619
- ` const _page = await import(${pageImportLiteral})`,
620
- ' const Page = _page?.default',
621
- ' if (typeof Page !== \'function\') {',
622
- ' throw new Error(`[@ossy/app] Page must export default as a function component`)',
623
- ' }',
624
- ' const rootTree = createElement(Page, initialConfig)',
625
- ' hydrateRoot(document, rootTree)',
626
- '}',
717
+ 'export default _page.default',
718
+ 'export const metadata = _page.metadata',
627
719
  '',
628
- 'main().catch((err) => {',
629
- ' console.error(err)',
630
- " document.body.innerHTML = '<p style=\\'font-family:sans-serif;padding:1rem\\'>Hydration failed.</p>'",
631
- '})',
720
+ "if (typeof window !== 'undefined') {",
721
+ " const Page = _page.default",
722
+ " const config = window.__INITIAL_APP_CONFIG__ || {}",
723
+ " hydrateRoot(document, createElement(Page, config))",
724
+ '}',
632
725
  '',
633
726
  ].join('\n')
634
727
  }
@@ -827,6 +920,7 @@ export const build = async (cliArgs) => {
827
920
  pagesGeneratedPath,
828
921
  })
829
922
  writePageHydrateStubs(pageFiles, srcDir, ossyDir)
923
+ writePageSsrStubs(pageFiles, srcDir, ossyDir)
830
924
  const clientHydrateInput = buildClientHydrateInput(pageFiles, srcDir, ossyDir)
831
925
 
832
926
  const { apiOverviewFiles } = resolveApiSource({
@@ -844,8 +938,6 @@ export const build = async (cliArgs) => {
844
938
  ? configPath
845
939
  : path.resolve(scriptDir, 'default-config.js')
846
940
 
847
- const pagesEntryPath = path.join(ossyDir, OSSY_PAGES_RUNTIME_BASENAME)
848
-
849
941
  const useDashboard = Object.keys(clientHydrateInput).length > 0
850
942
  const overviewSnap = getBuildOverviewSnapshot({
851
943
  pagesSourcePath: pagesGeneratedPath,
@@ -876,11 +968,6 @@ export const build = async (cliArgs) => {
876
968
  },
877
969
  })
878
970
  dashboard.start()
879
- if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
880
- dashboard.pushLog(
881
- `[resource-templates] merged ${resourceTemplatesResult.count} → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
882
- )
883
- }
884
971
  } else {
885
972
  console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild\x1b[0m')
886
973
  printBuildOverview({
@@ -889,28 +976,23 @@ export const build = async (cliArgs) => {
889
976
  configPath,
890
977
  pageFiles,
891
978
  })
892
- if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
893
- console.log(
894
- `[@ossy/app][resource-templates] merged ${resourceTemplatesResult.count} template(s) → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
895
- )
896
- }
897
979
  }
898
980
 
899
- const warnSink = dashboard
900
- ? (w) => {
901
- dashboard.pushLog(`rollup: ${w.message}`)
902
- }
903
- : 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
+ }
904
986
 
905
987
  const { taskOverviewFiles } = resolveTaskSource({ srcDir, buildPath })
906
988
  await compileOssyNodeArtifacts({
907
989
  pageFiles,
908
990
  srcDir,
909
991
  ossyDir,
992
+ buildPath,
910
993
  apiFiles: apiOverviewFiles,
911
994
  taskFiles: taskOverviewFiles,
912
995
  nodeEnv: 'production',
913
- onWarn: warnSink,
914
996
  })
915
997
 
916
998
  writeAppRuntimeShims({
@@ -929,8 +1011,6 @@ export const build = async (cliArgs) => {
929
1011
  copyPublicFrom: publicDir,
930
1012
  buildPath,
931
1013
  nodeEnv: 'production',
932
- pagesEntryPath,
933
- configSourcePath,
934
1014
  createClientRollupPlugins: createOssyClientRollupPlugins,
935
1015
  minifyBrowserStaticChunks,
936
1016
  reporter: dashboard,
@@ -940,8 +1020,5 @@ export const build = async (cliArgs) => {
940
1020
  }
941
1021
  }
942
1022
 
943
- if (useDashboard && dashboard) {
944
- console.log(' \x1b[2m────────────────────────────────────────\x1b[0m')
945
- }
946
1023
  console.log(' \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild finished\x1b[0m\n')
947
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