@ossy/app 1.11.1 → 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/README.md CHANGED
@@ -23,7 +23,7 @@ export default () => <h1>About</h1>
23
23
 
24
24
  For a single-file setup, use `src/pages.jsx` (legacy).
25
25
 
26
- Split pages are merged into **`build/.ossy/pages.generated.jsx`** (next to other build output) during `dev` / `build`.
26
+ During `dev` / `build`, the tooling writes **JSON manifests** under **`build/.ossy/`**: **`pages.generated.json`** (route ids, default paths, and `sourceFile` paths), **`pages.bundle.json`** (compiled `page-modules/<id>.mjs` paths), plus the same pattern for API and tasks (`*.generated.json` / `*.bundle.json`). Small **`*.runtime.mjs`** loaders (copied from `@ossy/app`) dynamically `import()` those compiled modules at runtime.
27
27
 
28
28
  **Client JS (per-page):** For each `*.page.jsx`, the build emits **`build/.ossy/hydrate-<pageId>.jsx`** → **`public/static/hydrate-<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.).
29
29
 
@@ -51,7 +51,7 @@ Define HTTP handlers as an array of `{ id, path, handle(req, res) }` objects (sa
51
51
 
52
52
  Add any number of `*.api.js` (or `.api.mjs` / `.api.cjs`) files under `src/` (nested dirs allowed). Each file’s **default export** is either one route object or an array of routes. Files are merged in lexicographic path order.
53
53
 
54
- Build/dev always writes **`build/.ossy/api.generated.js`** (typically gitignored with `build/`). With no API files it exports an empty array.
54
+ Build/dev writes **`build/.ossy/api.generated.json`** (discovered source paths) and **`api.bundle.json`** (Rollup outputs under **`api-modules/`**). The server imports **`api.runtime.mjs`**, which loads those modules. With no API files, both JSON files list empty arrays.
55
55
 
56
56
  Example `src/health.api.js`:
57
57
 
@@ -69,7 +69,7 @@ API routes are matched before the app is rendered. The router supports dynamic s
69
69
 
70
70
  ## Background worker tasks (`*.task.js`)
71
71
 
72
- For long-running job processors (separate from the SSR server), use **`npx @ossy/cli build --worker`** in a package that only needs the worker. It uses the same Rollup + Babel pipeline as `ossy build`, discovers **`*.task.js`** (and `.task.mjs` / `.task.cjs`) under `src/` (or `--pages <dir>`), and writes **`build/.ossy/tasks.generated.js`** when needed—same idea as `*.api.js` and the generated API bundle.
72
+ For long-running job processors (separate from the SSR server), use **`npx @ossy/cli build --worker`** in a package that only needs the worker. It uses the same Rollup + Babel pipeline as `ossy build`, discovers **`*.task.js`** (and `.task.mjs` / `.task.cjs`) under `src/` (or `--pages <dir>`), and writes **`tasks.generated.json`** / **`tasks.bundle.json`** plus **`tasks.runtime.mjs`**—the same metadata + per-file compile pattern as API routes.
73
73
 
74
74
  Optional legacy aggregate: **`src/tasks.js`** default export is merged **first**, then each `*.task.js` in path order.
75
75
 
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
+
5
+ const __ossyDir = path.dirname(fileURLToPath(import.meta.url))
6
+
7
+ function normalizeApiExport (mod) {
8
+ const d = mod?.default
9
+ if (d == null) return []
10
+ return Array.isArray(d) ? d : [d]
11
+ }
12
+
13
+ const { modules } = JSON.parse(fs.readFileSync(path.join(__ossyDir, 'api.bundle.json'), 'utf8'))
14
+
15
+ const out = []
16
+ for (const rel of modules) {
17
+ const abs = path.resolve(__ossyDir, rel)
18
+ const mod = await import(pathToFileURL(abs).href)
19
+ out.push(...normalizeApiExport(mod))
20
+ }
21
+
22
+ export default out
package/cli/build.js CHANGED
@@ -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,9 +230,10 @@ 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, onWarn }) {
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 }),
217
238
  onwarn (warning, defaultHandler) {
218
239
  if (onWarn) {
@@ -231,14 +252,19 @@ export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv, onW
231
252
  }
232
253
 
233
254
  /**
234
- * 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.
235
256
  */
236
- export function writeAppRuntimeShims ({ middlewareSourcePath, ossyDir }) {
257
+ export function writeAppRuntimeShims ({ middlewareSourcePath, configSourcePath, ossyDir }) {
237
258
  const mwHref = url.pathToFileURL(path.resolve(middlewareSourcePath)).href
238
259
  fs.writeFileSync(
239
260
  path.join(ossyDir, OSSY_MIDDLEWARE_RUNTIME_BASENAME),
240
261
  `// Generated by @ossy/app — do not edit\nexport { default } from '${mwHref}'\n`
241
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
+ )
242
268
  }
243
269
 
244
270
  /**
@@ -258,6 +284,18 @@ export function copyOssyAppRuntime ({ scriptDir, buildPath }) {
258
284
  }
259
285
  fs.copyFileSync(path.join(scriptDir, 'worker-entry.js'), path.join(buildPath, 'worker.js'))
260
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
+ )
261
299
  }
262
300
 
263
301
  /**
@@ -281,107 +319,157 @@ export function discoverFilesByPattern (srcDir, filePattern) {
281
319
  return files.sort()
282
320
  }
283
321
 
284
- /**
285
- * Merges every `*.api.js` (and `.api.mjs` / `.api.cjs`) under `src/` into one default export array
286
- * for the Ossy API router ({ id, path, handle }). With no API files, emits `export default []`.
287
- */
288
- export function generateApiModule ({ generatedPath, apiFiles }) {
289
- if (apiFiles.length === 0) {
290
- return [
291
- '// Generated by @ossy/app — do not edit',
292
- '',
293
- 'export default []',
294
- '',
295
- ].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, '/')),
296
340
  }
297
- const lines = [
298
- '// Generated by @ossy/app — do not edit',
299
- '',
300
- ]
301
- apiFiles.forEach((f, i) => {
302
- const rel = relToGeneratedImport(generatedPath, f)
303
- lines.push(`import * as _api${i} from './${rel}'`)
304
- })
305
- lines.push(
306
- '',
307
- 'function _normalizeApiExport(mod) {',
308
- ' const d = mod?.default',
309
- ' if (d == null) return []',
310
- ' return Array.isArray(d) ? d : [d]',
311
- '}',
312
- '',
313
- 'export default [',
314
- )
315
- const parts = apiFiles.map((_, i) => ` ..._normalizeApiExport(_api${i}),`)
316
- lines.push(parts.join('\n'))
317
- lines.push(']')
318
- lines.push('')
319
- return lines.join('\n')
320
341
  }
321
342
 
322
343
  /**
323
- * Writes `build/.ossy/api.generated.js` and returns its path for `@ossy/api/source-file`.
324
- * 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}.
325
346
  */
326
- export function resolveApiSource ({ srcDir, buildPath }) {
347
+ export function resolveApiSource ({ srcDir, buildPath, cwd = process.cwd() }) {
327
348
  ensureOssyGeneratedDir(buildPath)
328
349
  const generatedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_API_BASENAME)
329
350
  const apiFiles = discoverFilesByPattern(srcDir, API_FILE_PATTERN)
330
- fs.writeFileSync(
331
- generatedPath,
332
- generateApiModule({ generatedPath, apiFiles })
333
- )
334
- return { apiSourcePath: generatedPath, apiOverviewFiles: apiFiles }
351
+ writeOssyJson(generatedPath, buildApiManifestPayload(apiFiles, cwd))
352
+ return { apiGeneratedPath: generatedPath, apiOverviewFiles: apiFiles }
335
353
  }
336
354
 
337
355
  /**
338
- * Merges `src/tasks.js` (optional) and every `*.task.js` under `src/` into one default export array
339
- * of job handlers `{ type, handler }` for the Ossy worker.
356
+ * Writes `build/.ossy/tasks.generated.json` (sources only).
340
357
  */
341
- export function generateTaskModule ({ generatedPath, taskFiles }) {
342
- const lines = [
343
- '// Generated by @ossy/app — do not edit',
344
- '',
345
- ]
346
- taskFiles.forEach((f, i) => {
347
- const rel = relToGeneratedImport(generatedPath, f)
348
- lines.push(`import * as _task${i} from './${rel}'`)
349
- })
350
- lines.push(
351
- '',
352
- 'function _normalizeTaskExport(mod) {',
353
- ' const d = mod?.default',
354
- ' if (d == null) return []',
355
- ' return Array.isArray(d) ? d : [d]',
356
- '}',
357
- '',
358
- 'export default [',
359
- )
360
- const parts = []
361
- taskFiles.forEach((_, i) => {
362
- parts.push(` ..._normalizeTaskExport(_task${i}),`)
363
- })
364
- lines.push(parts.join('\n'))
365
- lines.push(']')
366
- lines.push('')
367
- return lines.join('\n')
368
- }
369
-
370
- /**
371
- * Writes `build/.ossy/tasks.generated.js` and returns its path (stable Rollup input; empty when no task files).
372
- */
373
- export function resolveTaskSource ({ srcDir, buildPath }) {
358
+ export function resolveTaskSource ({ srcDir, buildPath, cwd = process.cwd() }) {
374
359
  ensureOssyGeneratedDir(buildPath)
375
360
  const generatedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_TASKS_BASENAME)
376
361
  const taskFiles = discoverFilesByPattern(srcDir, TASK_FILE_PATTERN)
377
- fs.writeFileSync(
378
- generatedPath,
379
- generateTaskModule({
380
- generatedPath,
381
- 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,
382
391
  })
383
- )
384
- 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
+ })
385
473
  }
386
474
 
387
475
  export function filePathToRoute(filePath, srcDir) {
@@ -414,7 +502,7 @@ export function clientHydrateIdForPage (pageAbsPath, srcDir) {
414
502
 
415
503
  /**
416
504
  * One client entry per page: imports only that page module and hydrates the document.
417
- * 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.
418
506
  */
419
507
  export function generatePageHydrateModule ({ pageAbsPath, stubAbsPath, srcDir }) {
420
508
  const rel = relToGeneratedImport(stubAbsPath, pageAbsPath)
@@ -491,32 +579,39 @@ export function buildClientHydrateInput (pageFiles, srcDir, ossyDir) {
491
579
  return input
492
580
  }
493
581
 
494
- export function generatePagesModule (pageFiles, srcDir, generatedPath) {
495
- const lines = [
496
- "import React from 'react'",
497
- ...pageFiles.map((f, i) => {
498
- const rel = relToGeneratedImport(generatedPath, f)
499
- return `import * as _page${i} from './${rel}'`
500
- }),
501
- '',
502
- 'function toPage(mod, derived) {',
503
- ' const meta = mod?.metadata || {}',
504
- " const def = mod?.default",
505
- " if (typeof def === 'function') {",
506
- " return { ...derived, ...meta, element: React.createElement(def) }",
507
- ' }',
508
- " return { ...derived, ...meta, ...(def || {}) }",
509
- '}',
510
- '',
511
- 'export default [',
512
- ...pageFiles.map((f, i) => {
513
- const { id, path: defaultPath } = filePathToRoute(f, srcDir)
514
- const pathStr = JSON.stringify(defaultPath)
515
- return ` toPage(_page${i}, { id: '${id}', path: ${pathStr} }),`
516
- }),
517
- ']',
518
- ]
519
- 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
+ }
520
615
  }
521
616
 
522
617
  export function parsePagesFromSource(filePath) {
@@ -557,7 +652,6 @@ export function parsePagesFromSource(filePath) {
557
652
  */
558
653
  export function getBuildOverviewSnapshot ({
559
654
  pagesSourcePath,
560
- apiSourcePath,
561
655
  apiOverviewFiles = [],
562
656
  configPath,
563
657
  pageFiles,
@@ -567,17 +661,14 @@ export function getBuildOverviewSnapshot ({
567
661
  const configRel = fs.existsSync(configPath) ? rel(configPath) : null
568
662
 
569
663
  const pages = pageFiles?.length
570
- ? pageFiles.map((f) => filePathToRoute(f, srcDir))
571
- : parsePagesFromSource(pagesSourcePath)
572
-
573
- const apiFilesToScan =
574
- apiOverviewFiles?.length > 0
575
- ? apiOverviewFiles
576
- : fs.existsSync(apiSourcePath)
577
- ? [apiSourcePath]
578
- : []
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
+
579
670
  const apiRoutes = []
580
- for (const f of apiFilesToScan) {
671
+ for (const f of apiOverviewFiles) {
581
672
  if (fs.existsSync(f)) apiRoutes.push(...parsePagesFromSource(f))
582
673
  }
583
674
 
@@ -633,24 +724,21 @@ export const build = async (cliArgs) => {
633
724
  log: false,
634
725
  })
635
726
 
636
- 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)
637
729
 
638
- fs.writeFileSync(
730
+ writePagesManifest({
731
+ pageFiles,
732
+ srcDir,
639
733
  pagesGeneratedPath,
640
- generatePagesModule(pageFiles, srcDir, pagesGeneratedPath)
641
- )
642
- const ossyDir = ossyGeneratedDir(buildPath)
734
+ })
643
735
  writePageHydrateStubs(pageFiles, srcDir, ossyDir)
644
736
  const clientHydrateInput = buildClientHydrateInput(pageFiles, srcDir, ossyDir)
645
737
 
646
- const {
647
- apiSourcePath: resolvedApi,
648
- apiOverviewFiles,
649
- } = resolveApiSource({
738
+ const { apiOverviewFiles } = resolveApiSource({
650
739
  srcDir,
651
740
  buildPath,
652
741
  })
653
- let apiSourcePath = resolvedApi
654
742
  let middlewareSourcePath = path.resolve('src/middleware.js');
655
743
  const publicDir = path.resolve('public')
656
744
 
@@ -662,14 +750,11 @@ export const build = async (cliArgs) => {
662
750
  ? configPath
663
751
  : path.resolve(scriptDir, 'default-config.js')
664
752
 
665
- const pagesBundlePath = path.join(ossyDir, OSSY_PAGES_PRERENDER_BUNDLE)
666
- const apiBundlePath = path.join(ossyDir, OSSY_API_SERVER_BUNDLE)
667
- const tasksBundlePath = path.join(ossyDir, OSSY_TASKS_SERVER_BUNDLE)
753
+ const pagesEntryPath = path.join(ossyDir, OSSY_PAGES_RUNTIME_BASENAME)
668
754
 
669
755
  const useDashboard = Object.keys(clientHydrateInput).length > 0
670
756
  const overviewSnap = getBuildOverviewSnapshot({
671
757
  pagesSourcePath: pagesGeneratedPath,
672
- apiSourcePath,
673
758
  apiOverviewFiles,
674
759
  configPath,
675
760
  pageFiles,
@@ -706,7 +791,6 @@ export const build = async (cliArgs) => {
706
791
  console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild\x1b[0m')
707
792
  printBuildOverview({
708
793
  pagesSourcePath: pagesGeneratedPath,
709
- apiSourcePath,
710
794
  apiOverviewFiles,
711
795
  configPath,
712
796
  pageFiles,
@@ -724,28 +808,20 @@ export const build = async (cliArgs) => {
724
808
  }
725
809
  : undefined
726
810
 
727
- await bundleOssyNodeEntry({
728
- inputPath: pagesGeneratedPath,
729
- outputFile: pagesBundlePath,
730
- nodeEnv: 'production',
731
- onWarn: warnSink,
732
- })
733
- await bundleOssyNodeEntry({
734
- inputPath: apiSourcePath,
735
- outputFile: apiBundlePath,
736
- nodeEnv: 'production',
737
- onWarn: warnSink,
738
- })
739
- const { taskSourcePath } = resolveTaskSource({ srcDir, buildPath })
740
- await bundleOssyNodeEntry({
741
- inputPath: taskSourcePath,
742
- outputFile: tasksBundlePath,
811
+ const { taskOverviewFiles } = resolveTaskSource({ srcDir, buildPath })
812
+ await compileOssyNodeArtifacts({
813
+ pageFiles,
814
+ srcDir,
815
+ ossyDir,
816
+ apiFiles: apiOverviewFiles,
817
+ taskFiles: taskOverviewFiles,
743
818
  nodeEnv: 'production',
744
819
  onWarn: warnSink,
745
820
  })
746
821
 
747
822
  writeAppRuntimeShims({
748
823
  middlewareSourcePath,
824
+ configSourcePath,
749
825
  ossyDir,
750
826
  })
751
827
  copyOssyAppRuntime({ scriptDir, buildPath })
@@ -759,7 +835,7 @@ export const build = async (cliArgs) => {
759
835
  copyPublicFrom: publicDir,
760
836
  buildPath,
761
837
  nodeEnv: 'production',
762
- pagesBundlePath,
838
+ pagesEntryPath,
763
839
  configSourcePath,
764
840
  createClientRollupPlugins: createOssyClientRollupPlugins,
765
841
  minifyBrowserStaticChunks,
package/cli/dev.js CHANGED
@@ -7,11 +7,11 @@ import {
7
7
  filePathToRoute,
8
8
  discoverFilesByPattern,
9
9
  PAGE_FILE_PATTERN,
10
- generatePagesModule,
10
+ writePagesManifest,
11
11
  resolveApiSource,
12
12
  resolveTaskSource,
13
13
  resetOssyBuildDir,
14
- bundleOssyNodeEntry,
14
+ compileOssyNodeArtifacts,
15
15
  copyOssyAppRuntime,
16
16
  writeAppRuntimeShims,
17
17
  createOssyAppBundlePlugins,
@@ -21,11 +21,7 @@ import {
21
21
  clientHydrateIdForPage,
22
22
  ossyGeneratedDir,
23
23
  OSSY_GEN_PAGES_BASENAME,
24
- OSSY_GEN_API_BASENAME,
25
- OSSY_GEN_TASKS_BASENAME,
26
- OSSY_PAGES_PRERENDER_BUNDLE,
27
- OSSY_API_SERVER_BUNDLE,
28
- OSSY_TASKS_SERVER_BUNDLE,
24
+ OSSY_PAGES_RUNTIME_BASENAME,
29
25
  writeResourceTemplatesBarrelIfPresent,
30
26
  resourceTemplatesDir,
31
27
  OSSY_RESOURCE_TEMPLATES_OUT,
@@ -48,7 +44,7 @@ export const dev = async (cliArgs) => {
48
44
  const buildPath = path.resolve('build')
49
45
  const srcDir = path.resolve('src')
50
46
  const configPath = path.resolve(options['--config'] || 'src/config.js');
51
- const pageFiles = discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN)
47
+ let currentPageFiles = discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN)
52
48
 
53
49
  resetOssyBuildDir(buildPath)
54
50
 
@@ -58,35 +54,33 @@ export const dev = async (cliArgs) => {
58
54
  })
59
55
  let resourceTemplatesDevLogged = false
60
56
 
61
- const pagesGeneratedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_PAGES_BASENAME)
62
- fs.writeFileSync(
63
- pagesGeneratedPath,
64
- generatePagesModule(pageFiles, srcDir, pagesGeneratedPath)
65
- )
66
57
  const ossyDir = ossyGeneratedDir(buildPath)
67
- writePageHydrateStubs(pageFiles, srcDir, ossyDir)
68
- const clientHydrateInput = buildClientHydrateInput(pageFiles, srcDir, ossyDir)
69
-
70
- const {
71
- apiSourcePath: resolvedApi,
72
- apiOverviewFiles,
73
- } = resolveApiSource({
58
+ const pagesGeneratedPath = path.join(ossyDir, OSSY_GEN_PAGES_BASENAME)
59
+ writePagesManifest({
60
+ pageFiles: currentPageFiles,
74
61
  srcDir,
75
- buildPath,
62
+ pagesGeneratedPath,
76
63
  })
77
- let apiSourcePath = resolvedApi
78
- resolveTaskSource({ srcDir, buildPath })
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()
79
74
  let middlewareSourcePath = path.resolve(options['--middleware-source'] || 'src/middleware.js');
80
75
  const publicDir = path.resolve('public')
81
76
 
82
- if (pageFiles.length === 0) {
77
+ if (currentPageFiles.length === 0) {
83
78
  console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mdev\x1b[0m')
84
79
  printBuildOverview({
85
80
  pagesSourcePath: pagesGeneratedPath,
86
- apiSourcePath,
87
81
  apiOverviewFiles,
88
82
  configPath,
89
- pageFiles,
83
+ pageFiles: currentPageFiles,
90
84
  })
91
85
  if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
92
86
  console.log(
@@ -103,29 +97,21 @@ export const dev = async (cliArgs) => {
103
97
  ? configPath
104
98
  : path.resolve(scriptDir, 'default-config.js')
105
99
 
106
- const pagesBundlePath = path.join(ossyDir, OSSY_PAGES_PRERENDER_BUNDLE)
107
- const apiBundlePath = path.join(ossyDir, OSSY_API_SERVER_BUNDLE)
108
- const tasksBundlePath = path.join(ossyDir, OSSY_TASKS_SERVER_BUNDLE)
109
- const tasksGeneratedPath = path.join(ossyDir, OSSY_GEN_TASKS_BASENAME)
100
+ const pagesEntryPath = path.join(ossyDir, OSSY_PAGES_RUNTIME_BASENAME)
110
101
 
111
102
  const runNodeBundles = async () => {
112
- await bundleOssyNodeEntry({
113
- inputPath: pagesGeneratedPath,
114
- outputFile: pagesBundlePath,
115
- nodeEnv: 'development',
116
- })
117
- await bundleOssyNodeEntry({
118
- inputPath: apiSourcePath,
119
- outputFile: apiBundlePath,
120
- nodeEnv: 'development',
121
- })
122
- await bundleOssyNodeEntry({
123
- inputPath: tasksGeneratedPath,
124
- outputFile: tasksBundlePath,
103
+ refreshApiTaskManifests()
104
+ await compileOssyNodeArtifacts({
105
+ pageFiles: currentPageFiles,
106
+ srcDir,
107
+ ossyDir,
108
+ apiFiles: apiOverviewFiles,
109
+ taskFiles: taskOverviewFiles,
125
110
  nodeEnv: 'development',
126
111
  })
127
112
  writeAppRuntimeShims({
128
113
  middlewareSourcePath,
114
+ configSourcePath,
129
115
  ossyDir,
130
116
  })
131
117
  copyOssyAppRuntime({ scriptDir, buildPath })
@@ -194,38 +180,7 @@ export const dev = async (cliArgs) => {
194
180
  }
195
181
 
196
182
  const nodeWatchOpts = { watch: { clearScreen: false } }
197
- const watchConfigs = [
198
- {
199
- input: pagesGeneratedPath,
200
- output: {
201
- file: pagesBundlePath,
202
- format: 'esm',
203
- inlineDynamicImports: true,
204
- },
205
- plugins: createOssyAppBundlePlugins({ nodeEnv: 'development' }),
206
- ...nodeWatchOpts,
207
- },
208
- {
209
- input: apiSourcePath,
210
- output: {
211
- file: apiBundlePath,
212
- format: 'esm',
213
- inlineDynamicImports: true,
214
- },
215
- plugins: createOssyAppBundlePlugins({ nodeEnv: 'development' }),
216
- ...nodeWatchOpts,
217
- },
218
- {
219
- input: tasksGeneratedPath,
220
- output: {
221
- file: tasksBundlePath,
222
- format: 'esm',
223
- inlineDynamicImports: true,
224
- },
225
- plugins: createOssyAppBundlePlugins({ nodeEnv: 'development' }),
226
- ...nodeWatchOpts,
227
- },
228
- ]
183
+ const watchConfigs = []
229
184
  if (Object.keys(clientHydrateInput).length > 0) {
230
185
  watchConfigs.push({
231
186
  input: clientHydrateInput,
@@ -233,6 +188,30 @@ export const dev = async (cliArgs) => {
233
188
  plugins: clientPlugins,
234
189
  watch: { clearScreen: false },
235
190
  })
191
+ } else {
192
+ watchConfigs.push({
193
+ input: '\0ossy-dev-noop',
194
+ output: {
195
+ file: path.join(ossyDir, '.dev-noop-out.mjs'),
196
+ format: 'esm',
197
+ inlineDynamicImports: true,
198
+ },
199
+ plugins: [
200
+ {
201
+ name: 'ossy-dev-noop',
202
+ resolveId (id) {
203
+ if (id === '\0ossy-dev-noop') return id
204
+ return null
205
+ },
206
+ load (id) {
207
+ if (id === '\0ossy-dev-noop') return 'export default 0\n'
208
+ return null
209
+ },
210
+ },
211
+ ...createOssyAppBundlePlugins({ nodeEnv: 'development' }),
212
+ ],
213
+ ...nodeWatchOpts,
214
+ })
236
215
  }
237
216
  const watcher = watch(watchConfigs)
238
217
 
@@ -251,22 +230,22 @@ export const dev = async (cliArgs) => {
251
230
  if (event.code === 'END') {
252
231
  writeAppRuntimeShims({
253
232
  middlewareSourcePath,
233
+ configSourcePath,
254
234
  ossyDir,
255
235
  })
256
236
  copyOssyAppRuntime({ scriptDir, buildPath })
257
- if (pageFiles.length > 0) {
237
+ if (currentPageFiles.length > 0) {
258
238
  const pageIds = [
259
- ...new Set(pageFiles.map((f) => clientHydrateIdForPage(f, srcDir))),
239
+ ...new Set(currentPageFiles.map((f) => clientHydrateIdForPage(f, srcDir))),
260
240
  ].sort()
261
241
  const overviewSnap = getBuildOverviewSnapshot({
262
242
  pagesSourcePath: pagesGeneratedPath,
263
- apiSourcePath,
264
243
  apiOverviewFiles,
265
244
  configPath,
266
- pageFiles,
245
+ pageFiles: currentPageFiles,
267
246
  })
268
247
  const idToPath = Object.fromEntries(
269
- pageFiles.map((f) => [
248
+ currentPageFiles.map((f) => [
270
249
  clientHydrateIdForPage(f, srcDir),
271
250
  filePathToRoute(f, srcDir).path,
272
251
  ])
@@ -298,7 +277,7 @@ export const dev = async (cliArgs) => {
298
277
  try {
299
278
  await prerenderReactTask.handler({
300
279
  op: 'prerenderPagesParallel',
301
- pagesBundlePath,
280
+ pagesEntryPath,
302
281
  configSourcePath,
303
282
  publicDir: path.join(buildPath, 'public'),
304
283
  reporter,
@@ -315,37 +294,34 @@ export const dev = async (cliArgs) => {
315
294
 
316
295
  const regenApiGenerated = () => {
317
296
  if (options['--api-source']) return
318
- resolveApiSource({
319
- srcDir,
320
- buildPath,
321
- })
322
- const gen = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_API_BASENAME)
323
- if (fs.existsSync(gen) && typeof watcher?.invalidate === 'function') {
324
- watcher.invalidate(gen)
325
- }
297
+ void runNodeBundles().then(() => scheduleRestart())
326
298
  }
327
299
 
328
300
  const regenTasksGenerated = () => {
329
- resolveTaskSource({ srcDir, buildPath })
330
- if (fs.existsSync(tasksGeneratedPath) && typeof watcher?.invalidate === 'function') {
331
- watcher.invalidate(tasksGeneratedPath)
332
- }
301
+ void runNodeBundles().then(() => scheduleRestart())
333
302
  }
334
303
 
335
304
  fs.watch(srcDir, { recursive: true }, (eventType, filename) => {
336
305
  if (!filename) return
337
306
  if (/\.page\.(jsx?|tsx?)$/.test(filename)) {
338
307
  const refreshedPageFiles = discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN)
308
+ currentPageFiles = refreshedPageFiles
339
309
  const regenPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_PAGES_BASENAME)
340
- fs.writeFileSync(regenPath, generatePagesModule(refreshedPageFiles, srcDir, regenPath))
310
+ writePagesManifest({
311
+ pageFiles: refreshedPageFiles,
312
+ srcDir,
313
+ pagesGeneratedPath: regenPath,
314
+ })
341
315
  writePageHydrateStubs(refreshedPageFiles, srcDir, ossyDir)
342
- if (typeof watcher?.invalidate === 'function') {
343
- watcher.invalidate(regenPath)
344
- for (const f of refreshedPageFiles) {
345
- const hid = clientHydrateIdForPage(f, srcDir)
346
- watcher.invalidate(path.join(ossyDir, `hydrate-${hid}.jsx`))
316
+ void runNodeBundles().then(() => {
317
+ if (typeof watcher?.invalidate === 'function') {
318
+ for (const f of refreshedPageFiles) {
319
+ const hid = clientHydrateIdForPage(f, srcDir)
320
+ watcher.invalidate(path.join(ossyDir, `hydrate-${hid}.jsx`))
321
+ }
347
322
  }
348
- }
323
+ scheduleRestart()
324
+ })
349
325
  }
350
326
  if (/\.api\.(mjs|cjs|js)$/.test(filename)) {
351
327
  regenApiGenerated()
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
+ import React from 'react'
5
+
6
+ const __ossyDir = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ function readJson (name) {
9
+ return JSON.parse(fs.readFileSync(path.join(__ossyDir, name), 'utf8'))
10
+ }
11
+
12
+ function toPage (mod, derived) {
13
+ const meta = mod?.metadata || {}
14
+ const def = mod?.default
15
+ if (typeof def === 'function') {
16
+ return { ...derived, ...meta, element: React.createElement(def) }
17
+ }
18
+ return { ...derived, ...meta, ...(def || {}) }
19
+ }
20
+
21
+ const { pages: metaPages } = readJson('pages.generated.json')
22
+ const { pages: bundlePages } = readJson('pages.bundle.json')
23
+
24
+ if (metaPages.length !== bundlePages.length) {
25
+ throw new Error(
26
+ '[@ossy/app][pages.runtime] pages.generated.json and pages.bundle.json must list the same number of pages'
27
+ )
28
+ }
29
+
30
+ const out = []
31
+ for (let i = 0; i < metaPages.length; i++) {
32
+ const derived = { id: metaPages[i].id, path: metaPages[i].path }
33
+ const rel = bundlePages[i].module
34
+ const abs = path.resolve(__ossyDir, rel)
35
+ const mod = await import(pathToFileURL(abs).href)
36
+ out.push(toPage(mod, derived))
37
+ }
38
+
39
+ export default out
@@ -2,7 +2,7 @@ import path from 'path'
2
2
  import url from 'url'
3
3
  import fs from 'fs'
4
4
  import { rollup } from 'rollup'
5
- import { BuildPage } from './render-page.task.js'
5
+ import { BuildPage, buildPrerenderAppConfig } from './render-page.task.js'
6
6
  import { pageIdFromHydrateEntryName } from './build-terminal.js'
7
7
 
8
8
  /**
@@ -24,35 +24,6 @@ function pathIsPrerenderable (routePath) {
24
24
  return true
25
25
  }
26
26
 
27
- /** Mirrors server-era app shell config with static defaults (no cookies / session). */
28
- export function buildPrerenderAppConfig ({
29
- buildTimeConfig,
30
- pageList,
31
- activeRouteId,
32
- urlPath,
33
- }) {
34
- const pages = pageList.map((page) => {
35
- const entry = {
36
- id: page?.id,
37
- path: page?.path,
38
- }
39
- if (activeRouteId != null && page?.id === activeRouteId) {
40
- entry.element = page?.element
41
- }
42
- return entry
43
- })
44
- return {
45
- ...buildTimeConfig,
46
- url: urlPath,
47
- theme: buildTimeConfig.theme || 'light',
48
- isAuthenticated: false,
49
- workspaceId: buildTimeConfig.workspaceId,
50
- apiUrl: buildTimeConfig.apiUrl,
51
- pages,
52
- sidebarPrimaryCollapsed: false,
53
- }
54
- }
55
-
56
27
  function copyPublicToBuild ({ copyPublicFrom, buildPath }) {
57
28
  if (!copyPublicFrom || !fs.existsSync(copyPublicFrom)) return
58
29
  const dest = path.join(buildPath, 'public')
@@ -170,13 +141,13 @@ async function prerenderOnePage ({
170
141
  }
171
142
 
172
143
  async function prerenderPagesParallel ({
173
- pagesBundlePath,
144
+ pagesEntryPath,
174
145
  configSourcePath,
175
146
  publicDir,
176
147
  reporter,
177
148
  }) {
178
149
  const cfgHref = url.pathToFileURL(path.resolve(configSourcePath)).href
179
- const pagesHref = url.pathToFileURL(path.resolve(pagesBundlePath)).href
150
+ const pagesHref = url.pathToFileURL(path.resolve(pagesEntryPath)).href
180
151
 
181
152
  const configModule = await import(cfgHref)
182
153
  const pagesModule = await import(pagesHref)
@@ -237,7 +208,7 @@ async function runProduction ({
237
208
  copyPublicFrom,
238
209
  buildPath,
239
210
  nodeEnv,
240
- pagesBundlePath,
211
+ pagesEntryPath,
241
212
  configSourcePath,
242
213
  createClientRollupPlugins,
243
214
  minifyBrowserStaticChunks,
@@ -259,7 +230,7 @@ async function runProduction ({
259
230
  })
260
231
 
261
232
  const { failures: prerenderFailures } = await prerenderPagesParallel({
262
- pagesBundlePath,
233
+ pagesEntryPath,
263
234
  configSourcePath,
264
235
  publicDir: path.join(buildPath, 'public'),
265
236
  reporter,
@@ -284,7 +255,7 @@ export default {
284
255
  }
285
256
  if (op === 'prerenderPagesParallel') {
286
257
  return prerenderPagesParallel({
287
- pagesBundlePath: input.pagesBundlePath,
258
+ pagesEntryPath: input.pagesEntryPath,
288
259
  configSourcePath: input.configSourcePath,
289
260
  publicDir: input.publicDir,
290
261
  reporter: input.reporter,
@@ -1,6 +1,38 @@
1
1
  import React, { cloneElement } from 'react'
2
2
  import { prerenderToNodeStream } from 'react-dom/static'
3
3
 
4
+ /**
5
+ * App shell config for SSR / prerender (mirrors client: theme, pages metadata, active route element).
6
+ */
7
+ export function buildPrerenderAppConfig ({
8
+ buildTimeConfig,
9
+ pageList,
10
+ activeRouteId,
11
+ urlPath,
12
+ isAuthenticated = false,
13
+ }) {
14
+ const pages = pageList.map((page) => {
15
+ const entry = {
16
+ id: page?.id,
17
+ path: page?.path,
18
+ }
19
+ if (activeRouteId != null && page?.id === activeRouteId) {
20
+ entry.element = page?.element
21
+ }
22
+ return entry
23
+ })
24
+ return {
25
+ ...buildTimeConfig,
26
+ url: urlPath,
27
+ theme: buildTimeConfig.theme || 'light',
28
+ isAuthenticated,
29
+ workspaceId: buildTimeConfig.workspaceId,
30
+ apiUrl: buildTimeConfig.apiUrl,
31
+ pages,
32
+ sidebarPrimaryCollapsed: false,
33
+ }
34
+ }
35
+
4
36
  /** Strips non-JSON content (e.g. React elements on `pages`) for the bootstrap script. */
5
37
  export function appConfigForBootstrap (appConfig) {
6
38
  if (!appConfig || typeof appConfig !== 'object') return appConfig
package/cli/server.js CHANGED
@@ -6,12 +6,17 @@ import { Router as OssyRouter } from '@ossy/router'
6
6
  import { ProxyInternal } from './proxy-internal.js'
7
7
  import cookieParser from 'cookie-parser'
8
8
 
9
- import ApiRoutes from './.ossy/api.bundle.js'
9
+ import ApiRoutes from './.ossy/api.runtime.mjs'
10
+ import pageRoutes from './.ossy/pages.runtime.mjs'
11
+ import buildTimeConfig from './.ossy/server-config.runtime.mjs'
12
+ import { BuildPage, buildPrerenderAppConfig } from './.ossy/render-page.task.js'
10
13
  import Middleware from './.ossy/middleware.runtime.js'
11
14
 
12
15
  /** API bundle default may be an empty array. */
13
16
  const apiRouteList = ApiRoutes ?? []
14
17
 
18
+ const sitePageList = Array.isArray(pageRoutes) ? pageRoutes : []
19
+
15
20
  const app = express();
16
21
 
17
22
  const currentDir = path.dirname(url.fileURLToPath(import.meta.url))
@@ -101,22 +106,57 @@ const middleware = [
101
106
 
102
107
  app.use(middleware)
103
108
 
104
- const Router = OssyRouter.of({
109
+ const apiRouter = OssyRouter.of({
105
110
  pages: apiRouteList,
106
111
  })
107
112
 
108
- app.all('*all', (req, res) => {
109
- const pathname = req.originalUrl
113
+ const pageRouter = OssyRouter.of({
114
+ pages: sitePageList,
115
+ defaultLanguage: buildTimeConfig.defaultLanguage,
116
+ supportedLanguages: buildTimeConfig.supportedLanguages || [],
117
+ })
110
118
 
111
- const route = Router.getPageByUrl(pathname)
119
+ app.all('*all', async (req, res) => {
120
+ const requestUrl = req.originalUrl || '/'
112
121
 
113
- if (route && typeof route.handle === 'function') {
114
- console.log(`[@ossy/app][server] Handling API route: ${pathname}`)
115
- route.handle(req, res)
116
- return
117
- }
122
+ try {
123
+ const apiRoute = apiRouter.getPageByUrl(requestUrl)
124
+ if (apiRoute && typeof apiRoute.handle === 'function') {
125
+ console.log(`[@ossy/app][server] Handling API route: ${requestUrl}`)
126
+ apiRoute.handle(req, res)
127
+ return
128
+ }
129
+
130
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
131
+ res.status(404).send('Not found')
132
+ return
133
+ }
134
+
135
+ const pageRoute = pageRouter.getPageByUrl(requestUrl)
136
+ if (pageRoute?.element) {
137
+ const appConfig = buildPrerenderAppConfig({
138
+ buildTimeConfig,
139
+ pageList: sitePageList,
140
+ activeRouteId: pageRoute.id,
141
+ urlPath: requestUrl,
142
+ isAuthenticated: !!req.isAuthenticated,
143
+ })
144
+ const html = await BuildPage.handle({
145
+ route: pageRoute,
146
+ appConfig,
147
+ isDevReloadEnabled,
148
+ })
149
+ res.status(200).type('html').send(html)
150
+ return
151
+ }
118
152
 
119
- res.status(404).send('Not found')
153
+ res.status(404).send('Not found')
154
+ } catch (err) {
155
+ console.error('[@ossy/app][server] Request handling failed:', err)
156
+ if (!res.headersSent) {
157
+ res.status(500).type('text').send('Internal Server Error')
158
+ }
159
+ }
120
160
  })
121
161
 
122
162
  app.listen(port, () => {
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
+
5
+ const __ossyDir = path.dirname(fileURLToPath(import.meta.url))
6
+
7
+ function normalizeTaskExport (mod) {
8
+ const d = mod?.default
9
+ if (d == null) return []
10
+ return Array.isArray(d) ? d : [d]
11
+ }
12
+
13
+ const { modules } = JSON.parse(fs.readFileSync(path.join(__ossyDir, 'tasks.bundle.json'), 'utf8'))
14
+
15
+ const out = []
16
+ for (const rel of modules) {
17
+ const abs = path.resolve(__ossyDir, rel)
18
+ const mod = await import(pathToFileURL(abs).href)
19
+ out.push(...normalizeTaskExport(mod))
20
+ }
21
+
22
+ export default out
@@ -1,5 +1,5 @@
1
1
  import 'dotenv/config'
2
- import taskHandlers from './.ossy/tasks.bundle.js'
2
+ import taskHandlers from './.ossy/tasks.runtime.mjs'
3
3
  import { runWorkerScheduler } from './worker-runtime.js'
4
4
 
5
5
  runWorkerScheduler(taskHandlers)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "1.11.1",
3
+ "version": "1.11.2",
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.1",
31
- "@ossy/design-system": "^1.11.1",
32
- "@ossy/pages": "^1.11.1",
33
- "@ossy/router": "^1.11.1",
34
- "@ossy/router-react": "^1.11.1",
35
- "@ossy/sdk": "^1.11.1",
36
- "@ossy/sdk-react": "^1.11.1",
37
- "@ossy/themes": "^1.11.1",
30
+ "@ossy/connected-components": "^1.11.2",
31
+ "@ossy/design-system": "^1.11.2",
32
+ "@ossy/pages": "^1.11.2",
33
+ "@ossy/router": "^1.11.2",
34
+ "@ossy/router-react": "^1.11.2",
35
+ "@ossy/sdk": "^1.11.2",
36
+ "@ossy/sdk-react": "^1.11.2",
37
+ "@ossy/themes": "^1.11.2",
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": "dfa200a3ea12c726db2054a0ab902844507716a0"
70
+ "gitHead": "3998c8b461e8b7317462c3d1d65b25b9454ba18a"
71
71
  }