@ossy/app 1.15.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/build.js CHANGED
@@ -1,298 +1,51 @@
1
- import path from 'path';
2
- import url from 'url';
3
- import fs from 'fs';
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import url from 'node:url'
4
4
  import { pathToFileURL } from 'node:url'
5
5
  import { createRequire } from 'node:module'
6
- import { rollup } from 'rollup';
7
- import babel from '@rollup/plugin-babel';
6
+ import arg from 'arg'
7
+ import { rollup } from 'rollup'
8
+ import babel from '@rollup/plugin-babel'
8
9
  import { nodeResolve as resolveDependencies } from '@rollup/plugin-node-resolve'
9
10
  import resolveCommonJsDependencies from '@rollup/plugin-commonjs'
10
- import { minify as minifyWithTerser } from 'terser'
11
- import json from "@rollup/plugin-json"
11
+ import json from '@rollup/plugin-json'
12
+ import replace from '@rollup/plugin-replace'
12
13
  import nodeExternals from 'rollup-plugin-node-externals'
13
- import copy from 'rollup-plugin-copy';
14
- import replace from '@rollup/plugin-replace';
15
- import arg from 'arg'
14
+ import { minify as minifyWithTerser } from 'terser'
16
15
 
17
16
  export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
18
17
  export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
19
18
  export const TASK_FILE_PATTERN = /\.task\.(mjs|cjs|js)$/
20
19
  const RESOURCE_TEMPLATE_FILE_PATTERN = /\.resource\.js$/
21
20
 
22
- /** Written next to `*.resource.js` under `src/resource-templates/` when that dir exists. */
23
- export const OSSY_RESOURCE_TEMPLATES_OUT = '.ossy-system-templates.generated.js'
24
-
25
- export function minifyBrowserStaticChunks () {
26
- return {
27
- name: 'minify-browser-static-chunks',
28
- async renderChunk (code, chunk, outputOptions) {
29
- const fileName = chunk.fileName
30
- if (!fileName || !fileName.startsWith('public/static/')) {
31
- return null
32
- }
33
- const useSourceMap =
34
- outputOptions.sourcemap === true || typeof outputOptions.sourcemap === 'string'
35
- const fmt = outputOptions.format
36
- const result = await minifyWithTerser(code, {
37
- sourceMap: useSourceMap,
38
- module: fmt === 'es' || fmt === 'esm',
39
- })
40
- const out = result.code ?? code
41
- if (useSourceMap && result.map) {
42
- const map =
43
- typeof result.map === 'string' ? JSON.parse(result.map) : result.map
44
- return { code: out, map }
45
- }
46
- return out
47
- },
48
- }
49
- }
50
-
51
- /** Subfolder under `build/` for generated pages/api/task entry stubs. */
52
21
  export const OSSY_GEN_DIRNAME = '.ossy'
53
- /** JSON-only manifests (sources + route metadata). */
54
22
  export const OSSY_GEN_PAGES_BASENAME = 'pages.generated.json'
55
23
  export const OSSY_GEN_API_BASENAME = 'api.generated.json'
56
24
  export const OSSY_GEN_TASKS_BASENAME = 'tasks.generated.json'
57
- /** JSON-only build artifact index (compiled module paths, aligned to generated manifests). */
58
- export const OSSY_PAGES_BUNDLE_BASENAME = 'pages.bundle.json'
59
- export const OSSY_API_BUNDLE_BASENAME = 'api.bundle.json'
60
- export const OSSY_TASKS_BUNDLE_BASENAME = 'tasks.bundle.json'
61
- /** Small Node loaders (not JSON) that `import()` compiled modules from the bundle manifests. */
62
- export const OSSY_PAGES_RUNTIME_BASENAME = 'pages.runtime.mjs'
63
- export const OSSY_API_RUNTIME_BASENAME = 'api.runtime.mjs'
64
- export const OSSY_TASKS_RUNTIME_BASENAME = 'tasks.runtime.mjs'
65
-
66
- export const OSSY_PAGE_MODULES_DIRNAME = 'page-modules'
67
-
68
- /** Tiny Rollup inputs that re-export `metadata` so per-page server bundles keep i18n paths. */
69
- export const OSSY_PAGE_SERVER_ENTRIES_DIRNAME = 'page-server-entries'
70
- export const OSSY_API_MODULES_DIRNAME = 'api-modules'
71
- export const OSSY_TASK_MODULES_DIRNAME = 'task-modules'
72
-
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
- ]
85
-
86
- /** Output directory (relative to buildPath) for SSR bundle. */
87
- export const OSSY_SSR_DIRNAME = 'ssr'
88
-
89
- /** Shared client hydrate entry filename under `.ossy/` */
25
+ export const OSSY_RESOURCE_TEMPLATES_OUT = 'resource-templates.generated.json'
26
+
90
27
  const HYDRATE_ENTRY_FILENAME = 'hydrate-entry.jsx'
91
- /** Shared SSR entry filename under `.ossy/` */
92
28
  const SSR_ENTRY_FILENAME = 'ssr-entry.mjs'
93
29
 
94
- export function ossyGeneratedDir (buildPath) {
30
+ function ossyGeneratedDir (buildPath) {
95
31
  return path.join(buildPath, OSSY_GEN_DIRNAME)
96
32
  }
97
33
 
98
- export function ensureOssyGeneratedDir (buildPath) {
34
+ function ensureOssyGeneratedDir (buildPath) {
99
35
  const dir = ossyGeneratedDir(buildPath)
100
36
  fs.mkdirSync(dir, { recursive: true })
101
37
  return dir
102
38
  }
103
39
 
104
- function relToGeneratedImport (generatedAbs, targetAbs) {
105
- return path.relative(path.dirname(generatedAbs), targetAbs).replace(/\\/g, '/')
106
- }
107
-
108
- /** Deletes the entire build output dir, then recreates `build/.ossy` (generated stubs are written next). */
109
- export function resetOssyBuildDir (buildPath) {
40
+ function resetBuildDir (buildPath) {
110
41
  fs.rmSync(buildPath, { recursive: true, force: true })
111
42
  ensureOssyGeneratedDir(buildPath)
112
43
  }
113
44
 
114
- export function resourceTemplatesDir (cwd = process.cwd()) {
115
- return path.join(cwd, 'src', 'resource-templates')
116
- }
117
-
118
- export function discoverResourceTemplateFiles (templatesDir) {
119
- if (!fs.existsSync(templatesDir) || !fs.statSync(templatesDir).isDirectory()) {
120
- return []
121
- }
122
- return fs
123
- .readdirSync(templatesDir)
124
- .filter((n) => RESOURCE_TEMPLATE_FILE_PATTERN.test(n))
125
- .map((n) => path.join(templatesDir, n))
126
- .sort((a, b) => path.basename(a).localeCompare(path.basename(b)))
127
- }
128
-
129
- export function generateResourceTemplatesBarrelSource ({ outputAbs, templateFilesAbs }) {
130
- const lines = [
131
- '// Generated by @ossy/app — do not edit',
132
- '',
133
- ]
134
- templateFilesAbs.forEach((f, i) => {
135
- const rel = relToGeneratedImport(outputAbs, f)
136
- lines.push(`import _resource${i} from './${rel}'`)
137
- })
138
- lines.push('')
139
- lines.push(
140
- '/** Built-in resource templates merged into every workspace (with imported templates) in the API. */'
141
- )
142
- lines.push('export const SystemTemplates = [')
143
- templateFilesAbs.forEach((_, i) => {
144
- lines.push(` _resource${i},`)
145
- })
146
- lines.push(']')
147
- lines.push('')
148
- return lines.join('\n')
149
- }
150
-
151
- /**
152
- * If `src/resource-templates/` exists, writes `.ossy-system-templates.generated.js` there.
153
- * No-op when the directory is missing (e.g. website packages). Runs during `build` / `dev`.
154
- */
155
- export function writeResourceTemplatesBarrelIfPresent ({ cwd = process.cwd(), log = true } = {}) {
156
- const dir = resourceTemplatesDir(cwd)
157
- if (!fs.existsSync(dir)) {
158
- return { wrote: false, count: 0, path: null }
159
- }
160
- const files = discoverResourceTemplateFiles(dir)
161
- const outAbs = path.join(dir, OSSY_RESOURCE_TEMPLATES_OUT)
162
- fs.writeFileSync(outAbs, generateResourceTemplatesBarrelSource({ outputAbs: outAbs, templateFilesAbs: files }), 'utf8')
163
- if (log) {
164
- console.log(
165
- `[@ossy/app][resource-templates] merged ${files.length} template(s) → ${path.relative(cwd, outAbs)}`
166
- )
167
- }
168
- return { wrote: true, count: files.length, path: outAbs }
169
- }
170
-
171
- /**
172
- * Rollup plugins for Node-side bundles (pages / API / tasks): externals + Babel JSX.
173
- */
174
- export function createOssyAppBundlePlugins ({ nodeEnv }) {
175
- return [
176
- replace({
177
- preventAssignment: true,
178
- 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
179
- }),
180
- json(),
181
- nodeExternals({
182
- deps: false,
183
- devDeps: true,
184
- peerDeps: false,
185
- packagePath: path.join(process.cwd(), 'package.json'),
186
- }),
187
- resolveCommonJsDependencies(),
188
- resolveDependencies({ preferBuiltins: true }),
189
- babel({
190
- babelHelpers: 'bundled',
191
- extensions: ['.jsx', '.tsx'],
192
- presets: [['@babel/preset-react', { runtime: 'automatic' }]],
193
- }),
194
- ]
195
- }
196
-
197
- /**
198
- * Rollup plugins for the combined SSR + client bundle.
199
- * `preferBuiltins: true` is correct for the SSR entry (uses `node:stream`); page component code
200
- * does not import Node built-ins so this is safe for the browser entry as well.
201
- * `copyPublicFrom` is handled separately before the Rollup call.
202
- */
203
- export function createCombinedBundlePlugins ({ nodeEnv }) {
204
- return [
205
- replace({
206
- preventAssignment: true,
207
- 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
208
- }),
209
- json(),
210
- nodeExternals({
211
- deps: false,
212
- devDeps: true,
213
- peerDeps: false,
214
- packagePath: path.join(process.cwd(), 'package.json'),
215
- }),
216
- resolveCommonJsDependencies(),
217
- resolveDependencies({ preferBuiltins: true }),
218
- babel({
219
- babelHelpers: 'bundled',
220
- extensions: ['.jsx', '.tsx'],
221
- presets: [['@babel/preset-react', { runtime: 'automatic' }]],
222
- }),
223
- ]
224
- }
225
-
226
- /** Bundles a single Node ESM file (inline dynamic imports) for SSR / API / tasks. */
227
- export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv, onWarn, external }) {
228
- const bundle = await rollup({
229
- input: inputPath,
230
- ...(external && external.length ? { external } : {}),
231
- plugins: createOssyAppBundlePlugins({ nodeEnv }),
232
- onwarn (warning, defaultHandler) {
233
- if (onWarn) {
234
- onWarn(warning)
235
- return
236
- }
237
- defaultHandler(warning)
238
- },
239
- })
240
- await bundle.write({
241
- file: outputFile,
242
- format: 'esm',
243
- inlineDynamicImports: true,
244
- })
245
- await bundle.close()
246
- }
247
-
248
- /**
249
- * Re-exports middleware and app config via `file:` URLs so `src/*.js` can keep relative imports.
250
- */
251
- export function writeAppRuntimeShims ({ middlewareSourcePath, configSourcePath, ossyDir }) {
252
- const mwHref = url.pathToFileURL(path.resolve(middlewareSourcePath)).href
253
- fs.writeFileSync(
254
- path.join(ossyDir, OSSY_MIDDLEWARE_RUNTIME_BASENAME),
255
- `// Generated by @ossy/app — do not edit\nexport { default } from '${mwHref}'\n`
256
- )
257
- const cfgHref = url.pathToFileURL(path.resolve(configSourcePath)).href
258
- fs.writeFileSync(
259
- path.join(ossyDir, OSSY_SERVER_CONFIG_RUNTIME_BASENAME),
260
- `// Generated by @ossy/app — do not edit\nexport { default } from '${cfgHref}'\n`
261
- )
262
- }
263
-
264
- /**
265
- * Copies framework runtime into `build/`: server, worker entry, proxy.
266
- * Middleware loads via `./.ossy/middleware.runtime.js`.
267
- */
268
- export function copyOssyAppRuntime ({ scriptDir, buildPath }) {
269
- const require = createRequire(import.meta.url)
270
- const platformServerPath = require.resolve('@ossy/platform/server')
271
- const platformWorkerPath = require.resolve('@ossy/platform/worker')
272
- const platformDir = path.dirname(platformServerPath)
273
- const platformWorkerDir = path.dirname(platformWorkerPath)
274
- for (const name of ['server.js', 'proxy-internal.js']) {
275
- fs.copyFileSync(path.join(platformDir, name), path.join(buildPath, name))
276
- }
277
- const taskRuntimeFiles = fs
278
- .readdirSync(scriptDir, { withFileTypes: true })
279
- .filter((ent) => ent.isFile() && ent.name.endsWith('.task.js'))
280
- .map((ent) => ent.name)
281
- for (const name of taskRuntimeFiles) {
282
- fs.copyFileSync(path.join(scriptDir, name), path.join(buildPath, name))
283
- }
284
- fs.copyFileSync(path.join(platformWorkerDir, 'worker-entry.js'), path.join(buildPath, 'worker.js'))
285
- fs.copyFileSync(path.join(platformWorkerDir, 'worker-runtime.js'), path.join(buildPath, 'worker-runtime.js'))
286
- const ossyOut = ossyGeneratedDir(buildPath)
287
- fs.copyFileSync(
288
- path.join(scriptDir, OSSY_RENDER_PAGE_RUNTIME_BASENAME),
289
- path.join(ossyOut, OSSY_RENDER_PAGE_RUNTIME_BASENAME)
290
- )
45
+ function relToGeneratedImport (generatedAbs, targetAbs) {
46
+ return path.relative(path.dirname(generatedAbs), targetAbs).replace(/\\/g, '/')
291
47
  }
292
48
 
293
- /**
294
- * Recursively lists files under `srcDir` whose basename matches `filePattern` (e.g. `/\.api\.js$/`).
295
- */
296
49
  export function discoverFilesByPattern (srcDir, filePattern) {
297
50
  const dir = path.resolve(srcDir)
298
51
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
@@ -311,162 +64,31 @@ export function discoverFilesByPattern (srcDir, filePattern) {
311
64
  return files.sort()
312
65
  }
313
66
 
314
- export function writeOssyJson (filePath, data) {
315
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
316
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
317
- }
318
-
319
- /** JSON manifest: discovered API source paths (posix, relative to `cwd`). */
320
- export function buildApiManifestPayload (apiFiles, cwd = process.cwd()) {
321
- return apiFiles.map((f) => path.relative(cwd, f).replace(/\\/g, '/'))
322
- }
323
-
324
- /** JSON manifest: discovered task source paths (posix, relative to `cwd`). */
325
67
  export function buildTasksManifestPayload (taskFiles, cwd = process.cwd()) {
326
68
  return taskFiles.map((f) => path.relative(cwd, f).replace(/\\/g, '/'))
327
69
  }
328
70
 
329
- /**
330
- * Writes `build/.ossy/api.generated.json` (sources only).
331
- * Compiled modules + `api.bundle.json` are produced by {@link compileOssyNodeArtifacts}.
332
- */
333
- export function resolveApiSource ({ srcDir, buildPath, cwd = process.cwd() }) {
334
- ensureOssyGeneratedDir(buildPath)
335
- const generatedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_API_BASENAME)
336
- const apiFiles = discoverFilesByPattern(srcDir, API_FILE_PATTERN)
337
- writeOssyJson(generatedPath, buildApiManifestPayload(apiFiles, cwd))
338
- return { apiGeneratedPath: generatedPath, apiOverviewFiles: apiFiles }
339
- }
340
-
341
- /**
342
- * Writes `build/.ossy/tasks.generated.json` (sources only).
343
- */
344
- export function resolveTaskSource ({ srcDir, buildPath, cwd = process.cwd() }) {
345
- ensureOssyGeneratedDir(buildPath)
346
- const generatedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_TASKS_BASENAME)
347
- const taskFiles = discoverFilesByPattern(srcDir, TASK_FILE_PATTERN)
348
- writeOssyJson(generatedPath, buildTasksManifestPayload(taskFiles, cwd))
349
- return { tasksGeneratedPath: generatedPath, taskOverviewFiles: taskFiles }
350
- }
351
-
352
- export async function compilePageServerModules ({
353
- pageFiles,
354
- srcDir,
355
- ossyDir,
356
- nodeEnv,
357
- onWarn,
358
- }) {
359
- const modsDir = path.join(ossyDir, OSSY_PAGE_MODULES_DIRNAME)
360
- const entriesDir = path.join(ossyDir, OSSY_PAGE_SERVER_ENTRIES_DIRNAME)
361
- fs.rmSync(modsDir, { recursive: true, force: true })
362
- fs.rmSync(entriesDir, { recursive: true, force: true })
363
- if (pageFiles.length === 0) {
364
- return []
365
- }
366
- fs.mkdirSync(modsDir, { recursive: true })
367
- fs.mkdirSync(entriesDir, { recursive: true })
368
- const bundlePages = []
369
- for (const f of pageFiles) {
370
- const pageId = clientHydrateIdForPage(f, srcDir)
371
- const safeId = String(pageId).replace(/[^a-zA-Z0-9_-]+/g, '-') || 'page'
372
- const relPath = pageServerModuleRelPath(f, srcDir)
373
- const outName = path.posix.basename(relPath)
374
- const outFile = path.join(modsDir, outName)
375
- const stubPath = path.join(entriesDir, `${safeId}.mjs`)
376
- writePageServerRollupEntry({ pageAbsPath: f, stubPath })
377
- await bundleOssyNodeEntry({
378
- inputPath: stubPath,
379
- outputFile: outFile,
380
- nodeEnv,
381
- onWarn,
382
- external: OSSY_PAGE_SERVER_EXTERNAL,
383
- })
384
- bundlePages.push({
385
- id: pageId,
386
- module: relPath,
387
- })
388
- }
389
- return bundlePages
390
- }
391
-
392
- export async function compileApiServerModules ({ apiFiles, ossyDir, nodeEnv, onWarn }) {
393
- const modsDir = path.join(ossyDir, OSSY_API_MODULES_DIRNAME)
394
- fs.rmSync(modsDir, { recursive: true, force: true })
395
- if (apiFiles.length === 0) {
396
- return []
397
- }
398
- fs.mkdirSync(modsDir, { recursive: true })
399
- const routes = []
400
- for (let i = 0; i < apiFiles.length; i++) {
401
- const outName = `api-${i}.mjs`
402
- const outFile = path.join(modsDir, outName)
403
- await bundleOssyNodeEntry({
404
- inputPath: apiFiles[i],
405
- outputFile: outFile,
406
- nodeEnv,
407
- onWarn,
408
- })
409
- let meta = {}
410
- try {
411
- const mod = await import(pathToFileURL(outFile).href)
412
- meta = mod?.metadata && typeof mod.metadata === 'object' ? mod.metadata : {}
413
- } catch {
414
- // metadata unreadable — skip
71
+ export function parseIdPathPairsFromFile (filePath) {
72
+ try {
73
+ const content = fs.readFileSync(filePath, 'utf8')
74
+ const items = []
75
+ const idPathPattern = /\{\s*id\s*:\s*['"]([^'"]*)['"]\s*,\s*path\s*:\s*['"]([^'"]*)['"]/g
76
+ let m
77
+ while ((m = idPathPattern.exec(content)) !== null) {
78
+ items.push({ id: m[1], path: m[2] })
415
79
  }
416
- routes.push({ ...meta, module: `${OSSY_API_MODULES_DIRNAME}/${outName}` })
417
- }
418
- return routes
419
- }
420
-
421
- export async function compileTaskServerModules ({ taskFiles, ossyDir, nodeEnv, onWarn }) {
422
- const modsDir = path.join(ossyDir, OSSY_TASK_MODULES_DIRNAME)
423
- fs.rmSync(modsDir, { recursive: true, force: true })
424
- if (taskFiles.length === 0) {
80
+ return items
81
+ } catch {
425
82
  return []
426
83
  }
427
- fs.mkdirSync(modsDir, { recursive: true })
428
- const tasks = []
429
- for (let i = 0; i < taskFiles.length; i++) {
430
- const outName = `task-${i}.mjs`
431
- const outFile = path.join(modsDir, outName)
432
- await bundleOssyNodeEntry({
433
- inputPath: taskFiles[i],
434
- outputFile: outFile,
435
- nodeEnv,
436
- onWarn,
437
- })
438
- let meta = {}
439
- try {
440
- const mod = await import(pathToFileURL(outFile).href)
441
- meta = mod?.metadata && typeof mod.metadata === 'object' ? mod.metadata : {}
442
- } catch {
443
- // metadata unreadable — skip
444
- }
445
- tasks.push({ ...meta, module: `${OSSY_TASK_MODULES_DIRNAME}/${outName}` })
446
- }
447
- return tasks
448
84
  }
449
85
 
450
- /**
451
- * Compiles server-side artifacts: API modules and task modules.
452
- * SSR page bundles are now produced by the combined Rollup pass in compileCombinedBundle.
453
- */
454
- export async function compileOssyNodeArtifacts ({
455
- apiFiles,
456
- taskFiles,
457
- ossyDir,
458
- nodeEnv,
459
- onWarn,
460
- }) {
461
- const [apiRouteList, taskList] = await Promise.all([
462
- compileApiServerModules({ apiFiles, ossyDir, nodeEnv, onWarn }),
463
- compileTaskServerModules({ taskFiles, ossyDir, nodeEnv, onWarn }),
464
- ])
465
- writeOssyJson(path.join(ossyDir, OSSY_GEN_API_BASENAME), apiRouteList)
466
- writeOssyJson(path.join(ossyDir, OSSY_GEN_TASKS_BASENAME), taskList)
86
+ function writeJson (filePath, data) {
87
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
88
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
467
89
  }
468
90
 
469
- export function filePathToRoute(filePath, srcDir) {
91
+ function filePathToRoute (filePath, srcDir) {
470
92
  const rel = path.relative(srcDir, filePath).replace(/\\/g, '/')
471
93
  let pathPart = rel.replace(PAGE_FILE_PATTERN, '').replace(/\/index$/, '').replace(/\/home$/, '') || 'home'
472
94
  if (pathPart === 'index' || pathPart === 'home') pathPart = 'home'
@@ -475,30 +97,7 @@ export function filePathToRoute(filePath, srcDir) {
475
97
  return { id, path: routePath }
476
98
  }
477
99
 
478
- /**
479
- * Basename for `/static/<id>.js` (per-page client bundle) must match `route.id` after `metadata` is merged in `toPage`
480
- * (`{ ...derived, ...metadata }`). Uses a light `metadata` scan when possible.
481
- */
482
- export function clientHydrateIdForPage (pageAbsPath, srcDir) {
483
- const derived = filePathToRoute(pageAbsPath, srcDir)
484
- let src = ''
485
- try {
486
- src = fs.readFileSync(pageAbsPath, 'utf8')
487
- } catch {
488
- return derived.id
489
- }
490
- const metaIdx = src.indexOf('export const metadata')
491
- if (metaIdx === -1) return derived.id
492
- const after = src.slice(metaIdx)
493
- const idMatch = after.match(/\bid\s*:\s*['"]([^'"]+)['"]/)
494
- return idMatch ? idMatch[1] : derived.id
495
- }
496
-
497
- /**
498
- * Like `clientHydrateIdForPage` but also extracts `path` from `metadata`.
499
- * Used to embed the static page route map directly into the SSR entry.
500
- */
501
- export function pageRouteFromSource (pageAbsPath, srcDir) {
100
+ function pageRouteFromSource (pageAbsPath, srcDir) {
502
101
  const derived = filePathToRoute(pageAbsPath, srcDir)
503
102
  try {
504
103
  const src = fs.readFileSync(pageAbsPath, 'utf8')
@@ -507,9 +106,7 @@ export function pageRouteFromSource (pageAbsPath, srcDir) {
507
106
  const after = src.slice(metaIdx)
508
107
  const id = after.match(/\bid\s*:\s*['"]([^'"]+)['"]/)?.[1] ?? derived.id
509
108
  const strPath = after.match(/\bpath\s*:\s*['"]([^'"]+)['"]/)?.[1]
510
- if (strPath) {
511
- return { id, path: strPath }
512
- }
109
+ if (strPath) return { id, path: strPath }
513
110
  const pathObjBody = after.match(/\bpath\s*:\s*\{([\s\S]*?)\}/)?.[1]
514
111
  if (pathObjBody) {
515
112
  const languagePathEntries = [...pathObjBody.matchAll(/([A-Za-z0-9_]+)\s*:\s*['"]([^'"]+)['"]/g)]
@@ -526,82 +123,67 @@ export function pageRouteFromSource (pageAbsPath, srcDir) {
526
123
  }
527
124
  }
528
125
 
529
- /** Posix path relative to `build/.ossy/` for the compiled **Node** page module (SSR). */
530
- export function pageServerModuleRelPath (pageAbsPath, srcDir) {
531
- const pageId = clientHydrateIdForPage(pageAbsPath, srcDir)
532
- const safeId = String(pageId).replace(/[^a-zA-Z0-9_-]+/g, '-') || 'page'
533
- return `${OSSY_PAGE_MODULES_DIRNAME}/${safeId}.mjs`
534
- }
535
-
536
- export function pageSourceExportsMetadata (pageAbsPath) {
537
- try {
538
- const src = fs.readFileSync(pageAbsPath, 'utf8')
539
- return /\bexport\s+const\s+metadata\b/.test(src)
540
- } catch {
541
- return false
542
- }
126
+ function createNodePlugins (nodeEnv) {
127
+ return [
128
+ replace({
129
+ preventAssignment: true,
130
+ 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
131
+ }),
132
+ json(),
133
+ nodeExternals({
134
+ deps: false,
135
+ devDeps: true,
136
+ peerDeps: false,
137
+ packagePath: path.join(process.cwd(), 'package.json'),
138
+ }),
139
+ resolveCommonJsDependencies(),
140
+ resolveDependencies({ preferBuiltins: true }),
141
+ babel({
142
+ babelHelpers: 'bundled',
143
+ extensions: ['.jsx', '.tsx'],
144
+ presets: [['@babel/preset-react', { runtime: 'automatic' }]],
145
+ }),
146
+ ]
543
147
  }
544
148
 
545
- /**
546
- * Rollup tree-shakes `export const metadata` when the page file is the entry and `default`
547
- * never references it — breaks i18n `path` objects at runtime. Re-export from a shim entry
548
- * so `metadata` stays in the module graph.
549
- */
550
- export function writePageServerRollupEntry ({ pageAbsPath, stubPath }) {
551
- const rel = relToGeneratedImport(stubPath, pageAbsPath)
552
- const meta = pageSourceExportsMetadata(pageAbsPath)
553
- ? ', metadata'
554
- : ''
555
- fs.mkdirSync(path.dirname(stubPath), { recursive: true })
556
- fs.writeFileSync(
557
- stubPath,
558
- [
559
- '// Generated by @ossy/app — do not edit',
560
- `export { default${meta} } from '${rel}'`,
561
- '',
562
- ].join('\n'),
563
- 'utf8'
564
- )
149
+ async function bundleNodeEntry ({ inputPath, outputFile, nodeEnv }) {
150
+ const bundle = await rollup({
151
+ input: inputPath,
152
+ plugins: createNodePlugins(nodeEnv),
153
+ })
154
+ await bundle.write({
155
+ file: outputFile,
156
+ format: 'esm',
157
+ inlineDynamicImports: true,
158
+ })
159
+ await bundle.close()
565
160
  }
566
161
 
567
- /**
568
- * Generates a single shared hydrate entry that dynamically imports the active page at runtime.
569
- * Rollup processes this once, emitting React and shared deps as reusable chunks instead of
570
- * bundling them into every page separately.
571
- */
572
- export function generateHydrateEntry ({ pageFiles, srcDir, stubAbsPath }) {
162
+ function generateHydrateEntry ({ pageFiles, srcDir, stubAbsPath }) {
573
163
  const seenIds = new Set()
574
- const pageLines = []
575
- for (const f of pageFiles) {
576
- const hydrateId = clientHydrateIdForPage(f, srcDir)
577
- if (seenIds.has(hydrateId)) {
578
- throw new Error(
579
- `[@ossy/app] Duplicate client hydrate id "${hydrateId}" (${f}). Pages need unique ids.`
580
- )
164
+ const pageLines = pageFiles.map((f) => {
165
+ const { id } = pageRouteFromSource(f, srcDir)
166
+ if (seenIds.has(id)) {
167
+ throw new Error(`[@ossy/app] Duplicate page id "${id}" (${f}). Pages need unique ids.`)
581
168
  }
582
- seenIds.add(hydrateId)
169
+ seenIds.add(id)
583
170
  const rel = relToGeneratedImport(stubAbsPath, f)
584
- pageLines.push(` ${JSON.stringify(hydrateId)}: () => import('./${rel}'),`)
585
- }
586
-
171
+ return ` ${JSON.stringify(id)}: () => import('./${rel}'),`
172
+ })
587
173
  return [
588
174
  '// Generated by @ossy/app — do not edit',
589
- '',
590
175
  "import { createElement } from 'react'",
591
176
  "import { hydrateRoot } from 'react-dom/client'",
592
177
  "import { App } from '@ossy/connected-components'",
593
- '',
594
178
  'const config = window.__INITIAL_APP_CONFIG__ || {}',
595
- '',
596
179
  'const pages = {',
597
180
  ...pageLines,
598
181
  '}',
599
- '',
600
182
  'const load = pages[config.pageId]',
601
183
  'if (load) {',
602
184
  ' load().then((mod) => {',
603
185
  ' const Page = mod.default',
604
- " const metadata = mod.metadata || {}",
186
+ ' const metadata = mod.metadata || {}',
605
187
  ' function PageShell (props) {',
606
188
  " return createElement('html', { lang: props.defaultLanguage || 'en' },",
607
189
  " createElement('head', null,",
@@ -620,64 +202,34 @@ export function generateHydrateEntry ({ pageFiles, srcDir, stubAbsPath }) {
620
202
  ].join('\n')
621
203
  }
622
204
 
623
- /** Writes a single `hydrate-entry.jsx`; removes any stale per-page `hydrate-*.jsx` stubs first. */
624
- export function writeHydrateEntry (pageFiles, srcDir, ossyDir) {
625
- if (!fs.existsSync(ossyDir)) fs.mkdirSync(ossyDir, { recursive: true })
626
- for (const ent of fs.readdirSync(ossyDir, { withFileTypes: true })) {
627
- if (
628
- ent.isFile() &&
629
- ent.name.startsWith('hydrate-') &&
630
- ent.name.endsWith('.jsx') &&
631
- ent.name !== HYDRATE_ENTRY_FILENAME
632
- ) {
633
- fs.rmSync(path.join(ossyDir, ent.name), { force: true })
634
- }
635
- }
636
- const stubPath = path.join(ossyDir, HYDRATE_ENTRY_FILENAME)
637
- fs.writeFileSync(stubPath, generateHydrateEntry({ pageFiles, srcDir, stubAbsPath: stubPath }))
638
- }
639
-
640
- /**
641
- * Generates the single shared SSR entry that exports a static `pages` array and a
642
- * `renderPage(pageId, props, options)` function. Each page is dynamically imported so
643
- * Rollup can split out lazy chunks per page when code-splitting is enabled.
644
- */
645
- export function generateSsrEntry ({ pageFiles, srcDir, stubAbsPath }) {
205
+ function generateSsrEntry ({ pageFiles, srcDir, stubAbsPath }) {
646
206
  const seenIds = new Set()
647
207
  const pagesLiteral = []
648
208
  const pageModuleLines = []
649
-
650
209
  for (const f of pageFiles) {
651
210
  const { id, path: routePath } = pageRouteFromSource(f, srcDir)
652
211
  if (seenIds.has(id)) {
653
- throw new Error(
654
- `[@ossy/app] Duplicate page id "${id}" (${f}). Pages need unique ids.`
655
- )
212
+ throw new Error(`[@ossy/app] Duplicate page id "${id}" (${f}). Pages need unique ids.`)
656
213
  }
657
214
  seenIds.add(id)
658
215
  const rel = relToGeneratedImport(stubAbsPath, f)
659
216
  pagesLiteral.push(` { id: ${JSON.stringify(id)}, path: ${JSON.stringify(routePath)} },`)
660
217
  pageModuleLines.push(` ${JSON.stringify(id)}: () => import('./${rel}'),`)
661
218
  }
662
-
663
219
  return [
664
220
  '// Generated by @ossy/app — do not edit',
665
- '',
666
221
  "import { createElement } from 'react'",
667
222
  "import { renderToPipeableStream } from 'react-dom/server'",
668
223
  "import { Writable } from 'node:stream'",
669
224
  "import { App } from '@ossy/connected-components'",
670
- '',
671
225
  'export const pages = [',
672
226
  ...pagesLiteral,
673
227
  ']',
674
- '',
675
228
  'const pageModules = {',
676
229
  ...pageModuleLines,
677
230
  '}',
678
- '',
679
231
  'function PageShell (props) {',
680
- " const meta = props._pageMeta || {}",
232
+ ' const meta = props._pageMeta || {}',
681
233
  " return createElement('html', { lang: props.defaultLanguage || 'en' },",
682
234
  " createElement('head', null,",
683
235
  " createElement('meta', { charSet: 'utf-8' }),",
@@ -688,10 +240,9 @@ export function generateSsrEntry ({ pageFiles, srcDir, stubAbsPath }) {
688
240
  ' )',
689
241
  ' )',
690
242
  '}',
691
- '',
692
- 'export async function renderPage (pageId, props, options = {}) {',
243
+ 'export async function renderPage (pageId, props) {',
693
244
  ' const load = pageModules[pageId]',
694
- " if (!load) throw new Error(`[@ossy/app] Unknown page id: ${pageId}`)",
245
+ ' if (!load) throw new Error(`[@ossy/app] Unknown page id: ${pageId}`)',
695
246
  ' const mod = await load()',
696
247
  ' const merged = { ...props, _pageComponent: mod.default, _pageMeta: mod.metadata || {} }',
697
248
  ' return new Promise((resolve, reject) => {',
@@ -700,7 +251,6 @@ export function generateSsrEntry ({ pageFiles, srcDir, stubAbsPath }) {
700
251
  ' write (chunk, _enc, cb) { html += chunk.toString(); cb() },',
701
252
  ' })',
702
253
  ' const { pipe } = renderToPipeableStream(createElement(PageShell, merged), {',
703
- ' ...options,',
704
254
  ' onAllReady () { pipe(writable) },',
705
255
  ' onError (err) { reject(err) },',
706
256
  ' })',
@@ -711,262 +261,189 @@ export function generateSsrEntry ({ pageFiles, srcDir, stubAbsPath }) {
711
261
  ].join('\n')
712
262
  }
713
263
 
714
- /** Writes `ssr-entry.mjs` into ossyDir; removes any stale per-page SSR stubs first. */
715
- export function writeSsrEntry (pageFiles, srcDir, ossyDir) {
716
- if (!fs.existsSync(ossyDir)) fs.mkdirSync(ossyDir, { recursive: true })
717
- const stubPath = path.join(ossyDir, SSR_ENTRY_FILENAME)
718
- fs.writeFileSync(stubPath, generateSsrEntry({ pageFiles, srcDir, stubAbsPath: stubPath }))
719
- return stubPath
264
+ function writeAppRuntimeShims ({ middlewareSourcePath, configSourcePath, ossyDir }) {
265
+ writeJson(path.join(ossyDir, 'middleware.runtime.json'), {
266
+ modulePath: path.resolve(middlewareSourcePath),
267
+ })
268
+ writeJson(path.join(ossyDir, 'server-config.runtime.json'), {
269
+ modulePath: path.resolve(configSourcePath),
270
+ })
720
271
  }
721
272
 
722
- /**
723
- * Runs a single Rollup pass with both the SSR entry and the client hydrate entry as inputs.
724
- * Produces:
725
- * build/ssr/app.mjs — Node SSR bundle
726
- * build/public/static/app.js browser hydrate bundle
727
- * build/public/static/chunks/[name]-[hash].js shared chunks
728
- *
729
- * The SSR bundle imports shared chunks via relative paths (`../public/static/chunks/…`);
730
- * the browser loads the same physical files from `/static/chunks/`.
731
- */
732
- export async function compileCombinedBundle ({ ssrEntryPath, clientEntryPath, buildPath, nodeEnv, copyPublicFrom, onWarn }) {
733
- if (copyPublicFrom && fs.existsSync(copyPublicFrom)) {
734
- const destPublic = path.join(buildPath, 'public')
735
- fs.mkdirSync(destPublic, { recursive: true })
736
- const copyDir = (src, dest) => {
737
- fs.mkdirSync(dest, { recursive: true })
738
- for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
739
- const srcPath = path.join(src, ent.name)
740
- const destPath = path.join(dest, ent.name)
741
- if (ent.isDirectory()) copyDir(srcPath, destPath)
742
- else fs.copyFileSync(srcPath, destPath)
743
- }
744
- }
745
- copyDir(copyPublicFrom, destPublic)
273
+ function minifyBrowserStaticChunks () {
274
+ return {
275
+ name: 'minify-browser-static-chunks',
276
+ async renderChunk (code, chunk, outputOptions) {
277
+ const fileName = chunk.fileName
278
+ if (!fileName || !fileName.startsWith('public/static/')) return null
279
+ const result = await minifyWithTerser(code, {
280
+ sourceMap: outputOptions.sourcemap === true || typeof outputOptions.sourcemap === 'string',
281
+ module: outputOptions.format === 'es' || outputOptions.format === 'esm',
282
+ })
283
+ return result.code ?? code
284
+ },
746
285
  }
286
+ }
747
287
 
288
+ async function compileCombinedBundle ({ ssrEntryPath, clientEntryPath, buildPath, nodeEnv, copyPublicFrom }) {
289
+ if (copyPublicFrom && fs.existsSync(copyPublicFrom)) {
290
+ fs.cpSync(copyPublicFrom, path.join(buildPath, 'public'), { recursive: true, force: true })
291
+ }
748
292
  const bundle = await rollup({
749
293
  input: { server: ssrEntryPath, app: clientEntryPath },
750
- plugins: createCombinedBundlePlugins({ nodeEnv }),
751
- onwarn (warning, defaultHandler) {
752
- if (onWarn) { onWarn(warning); return }
753
- defaultHandler(warning)
754
- },
294
+ plugins: createNodePlugins(nodeEnv),
755
295
  })
756
296
  await bundle.write({
757
297
  dir: buildPath,
758
298
  format: 'esm',
759
- entryFileNames: (chunk) =>
760
- chunk.name === 'server'
761
- ? 'ssr/app.mjs'
762
- : 'public/static/app.js',
299
+ entryFileNames: (chunk) => (chunk.name === 'server' ? 'ssr/app.mjs' : 'public/static/app.js'),
763
300
  chunkFileNames: 'public/static/chunks/[name]-[hash].js',
764
301
  plugins: [minifyBrowserStaticChunks()],
765
302
  })
766
303
  await bundle.close()
767
304
  }
768
305
 
769
- /** JSON manifest: route ids, default paths, and page source paths (posix, relative to `cwd`). */
770
- export function buildPagesGeneratedPayload (pageFiles, srcDir, cwd = process.cwd()) {
771
- const pages = pageFiles.map((f) => {
772
- const { id, path: routePath } = pageRouteFromSource(f, srcDir)
773
- return {
774
- id,
775
- path: routePath,
776
- sourceFile: path.relative(cwd, f).replace(/\\/g, '/'),
777
- }
778
- })
779
- return pages
306
+ async function compileApiServerModules ({ apiFiles, ossyDir, nodeEnv }) {
307
+ const modsDir = path.join(ossyDir, 'api-modules')
308
+ fs.rmSync(modsDir, { recursive: true, force: true })
309
+ fs.mkdirSync(modsDir, { recursive: true })
310
+ const routes = []
311
+ for (let i = 0; i < apiFiles.length; i++) {
312
+ const outName = `api-${i}.mjs`
313
+ const outFile = path.join(modsDir, outName)
314
+ await bundleNodeEntry({ inputPath: apiFiles[i], outputFile: outFile, nodeEnv })
315
+ let meta = {}
316
+ try {
317
+ const mod = await import(pathToFileURL(outFile).href)
318
+ meta = mod?.metadata && typeof mod.metadata === 'object' ? mod.metadata : {}
319
+ } catch {}
320
+ routes.push({ ...meta, module: `api-modules/${outName}` })
321
+ }
322
+ return routes
780
323
  }
781
324
 
782
- export function writePagesManifest ({
783
- pageFiles,
784
- srcDir,
785
- pagesGeneratedPath,
786
- cwd = process.cwd(),
787
- }) {
788
- writeOssyJson(pagesGeneratedPath, buildPagesGeneratedPayload(pageFiles, srcDir, cwd))
325
+ async function compileTaskServerModules ({ taskFiles, ossyDir, nodeEnv }) {
326
+ const modsDir = path.join(ossyDir, 'task-modules')
327
+ fs.rmSync(modsDir, { recursive: true, force: true })
328
+ fs.mkdirSync(modsDir, { recursive: true })
329
+ const tasks = []
330
+ for (let i = 0; i < taskFiles.length; i++) {
331
+ const outName = `task-${i}.mjs`
332
+ const outFile = path.join(modsDir, outName)
333
+ await bundleNodeEntry({ inputPath: taskFiles[i], outputFile: outFile, nodeEnv })
334
+ let meta = {}
335
+ try {
336
+ const mod = await import(pathToFileURL(outFile).href)
337
+ meta = mod?.metadata && typeof mod.metadata === 'object' ? mod.metadata : {}
338
+ } catch {}
339
+ tasks.push({ ...meta, module: `task-modules/${outName}` })
340
+ }
341
+ return tasks
789
342
  }
790
343
 
791
- export function parsePagesFromManifestJson (manifestPath) {
792
- try {
793
- const data = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
794
- if (!Array.isArray(data)) return []
795
- return data.map((p) => ({
796
- id: p.id,
797
- path: p.path,
798
- ...(typeof p.module === 'string' ? { module: p.module } : {}),
799
- }))
800
- } catch {
801
- return []
802
- }
344
+ function writeGeneratedEntries ({ pageFiles, srcDir, ossyDir }) {
345
+ const ssrEntryPath = path.join(ossyDir, SSR_ENTRY_FILENAME)
346
+ const hydrateEntryPath = path.join(ossyDir, HYDRATE_ENTRY_FILENAME)
347
+ fs.writeFileSync(ssrEntryPath, generateSsrEntry({ pageFiles, srcDir, stubAbsPath: ssrEntryPath }))
348
+ fs.writeFileSync(hydrateEntryPath, generateHydrateEntry({ pageFiles, srcDir, stubAbsPath: hydrateEntryPath }))
349
+ return { ssrEntryPath, hydrateEntryPath }
803
350
  }
804
351
 
805
- /**
806
- * Best-effort scan of a source file for `{ id, path }` literals (e.g. `*.api.js` default export).
807
- * Used only for the build dashboard API list — **not** for page discovery (`*.page.jsx` only).
808
- */
809
- export function parseIdPathPairsFromFile (filePath) {
810
- try {
811
- const content = fs.readFileSync(filePath, 'utf8')
812
- const items = []
813
- const idPathPattern = /\{\s*id\s*:\s*['"]([^'"]*)['"]\s*,\s*path\s*:\s*['"]([^'"]*)['"]/g
814
- const pathObjPattern = /\{\s*path\s*:\s*\{\s*([^}]+)\}/g
815
- let m
816
- while ((m = idPathPattern.exec(content)) !== null) {
817
- items.push({ id: m[1], path: m[2] })
818
- }
819
- if (items.length === 0) {
820
- while ((m = pathObjPattern.exec(content)) !== null) {
821
- const pathStr = m[1].replace(/\s/g, '').replace(/:/g, ': ')
822
- items.push({ id: 'page', path: pathStr })
823
- }
824
- }
825
- return items
826
- } catch {
827
- return []
352
+ function copyPlatformRuntime ({ buildPath }) {
353
+ const require = createRequire(import.meta.url)
354
+ const platformServerPath = require.resolve('@ossy/platform/server')
355
+ const platformWorkerPath = require.resolve('@ossy/platform/worker')
356
+ const platformServerDir = path.dirname(platformServerPath)
357
+ const platformWorkerDir = path.dirname(platformWorkerPath)
358
+
359
+ for (const name of ['server.js', 'proxy-internal.js']) {
360
+ fs.copyFileSync(path.join(platformServerDir, name), path.join(buildPath, name))
828
361
  }
362
+ fs.copyFileSync(path.join(platformWorkerDir, 'worker-entry.js'), path.join(buildPath, 'worker.js'))
363
+ fs.copyFileSync(path.join(platformWorkerDir, 'worker-runtime.js'), path.join(buildPath, 'worker-runtime.js'))
829
364
  }
830
365
 
831
- /**
832
- * Same facts as the old build overview printout, for dashboards / plain logging.
833
- */
834
- export function getBuildOverviewSnapshot ({
835
- pagesSourcePath,
836
- apiOverviewFiles = [],
837
- configPath,
838
- pageFiles,
839
- }) {
840
- const rel = (p) => (p ? path.relative(process.cwd(), p) : undefined)
841
- const srcDir = path.resolve(process.cwd(), 'src')
842
- const configRel = fs.existsSync(configPath) ? rel(configPath) : null
843
-
844
- const pages = pageFiles?.length
845
- ? pageFiles.map((f) => {
846
- const { id, path: routePath } = filePathToRoute(f, srcDir)
847
- return { id: clientHydrateIdForPage(f, srcDir), path: routePath }
848
- })
849
- : parsePagesFromManifestJson(pagesSourcePath)
366
+ export function resourceTemplatesDir (cwd = process.cwd()) {
367
+ return path.join(cwd, 'src', 'resource-templates')
368
+ }
850
369
 
851
- const apiRoutes = []
852
- for (const f of apiOverviewFiles) {
853
- if (fs.existsSync(f)) apiRoutes.push(...parseIdPathPairsFromFile(f))
854
- }
370
+ export function discoverResourceTemplateFiles (templatesDir) {
371
+ if (!fs.existsSync(templatesDir) || !fs.statSync(templatesDir).isDirectory()) return []
372
+ return fs
373
+ .readdirSync(templatesDir)
374
+ .filter((n) => RESOURCE_TEMPLATE_FILE_PATTERN.test(n))
375
+ .map((n) => path.join(templatesDir, n))
376
+ .sort((a, b) => path.basename(a).localeCompare(path.basename(b)))
377
+ }
855
378
 
856
- return { configRel, pages, apiRoutes }
379
+ export function generateResourceTemplatesBarrelSource ({ outputAbs, templateFilesAbs }) {
380
+ const payload = templateFilesAbs.map((f) => relToGeneratedImport(outputAbs, f))
381
+ return `${JSON.stringify(payload, null, 2)}\n`
857
382
  }
858
383
 
859
- export function printBuildOverview (opts) {
860
- const { configRel, pages, apiRoutes } = getBuildOverviewSnapshot(opts)
861
- console.log('\n \x1b[1mBuild overview\x1b[0m')
862
- if (configRel) {
863
- console.log(` \x1b[36mConfig:\x1b[0m ${configRel}`)
864
- }
865
- console.log(' ' + '─'.repeat(50))
866
-
867
- if (pages.length > 0) {
868
- console.log(' \x1b[36mRoutes:\x1b[0m')
869
- const maxId = Math.max(6, ...pages.map((p) => String(p.id).length))
870
- const maxPath = Math.max(6, ...pages.map((p) => String(p.path).length))
871
- pages.forEach((p) => {
872
- const id = String(p.id).padEnd(maxId)
873
- const pathStr = String(p.path).padEnd(maxPath)
874
- console.log(` ${id} ${pathStr}`)
875
- })
876
- } else {
877
- console.log(' \x1b[33mRoutes:\x1b[0m (could not parse or empty)')
384
+ export function writeResourceTemplatesBarrelIfPresent ({ cwd = process.cwd(), log = true } = {}) {
385
+ const dir = resourceTemplatesDir(cwd)
386
+ if (!fs.existsSync(dir)) {
387
+ return { wrote: false, count: 0, path: null }
878
388
  }
879
-
880
- if (apiRoutes.length > 0) {
881
- console.log(' \x1b[36mAPI routes:\x1b[0m')
882
- apiRoutes.forEach((r) => {
883
- console.log(` ${r.id} ${r.path}`)
884
- })
389
+ const files = discoverResourceTemplateFiles(dir)
390
+ const outAbs = path.join(dir, OSSY_RESOURCE_TEMPLATES_OUT)
391
+ fs.writeFileSync(outAbs, generateResourceTemplatesBarrelSource({ outputAbs: outAbs, templateFilesAbs: files }), 'utf8')
392
+ if (log) {
393
+ console.log(`[@ossy/app][resource-templates] merged ${files.length} template(s) → ${path.relative(cwd, outAbs)}`)
885
394
  }
886
- console.log(' ' + '─'.repeat(50) + '\n')
395
+ return { wrote: true, count: files.length, path: outAbs }
887
396
  }
888
397
 
889
- export const build = async (cliArgs) => {
890
- const options = arg({
891
- '--config': String,
892
- '-c': '--config',
893
- }, { argv: cliArgs })
894
-
895
- const scriptDir = path.dirname(url.fileURLToPath(import.meta.url))
896
- const buildPath = path.resolve('build')
897
- const srcDir = path.resolve('src')
898
- const configPath = path.resolve(options['--config'] || 'src/config.js')
899
- const pageFiles = discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN)
900
- const publicDir = path.resolve('public')
901
-
902
- resetOssyBuildDir(buildPath)
903
-
904
- const resourceTemplatesResult = writeResourceTemplatesBarrelIfPresent({
905
- cwd: process.cwd(),
906
- log: false,
907
- })
908
-
909
- const ossyDir = ossyGeneratedDir(buildPath)
910
- const pagesGeneratedPath = path.join(ossyDir, OSSY_GEN_PAGES_BASENAME)
911
-
912
- writePagesManifest({
913
- pageFiles,
914
- srcDir,
915
- pagesGeneratedPath,
916
- })
917
-
918
- // Write generated entries (both SSR and client hydrate)
919
- const ssrEntryPath = writeSsrEntry(pageFiles, srcDir, ossyDir)
920
- writeHydrateEntry(pageFiles, srcDir, ossyDir)
921
- const clientEntryPath = path.join(ossyDir, HYDRATE_ENTRY_FILENAME)
922
-
923
- const { apiOverviewFiles } = resolveApiSource({ srcDir, buildPath })
924
- let middlewareSourcePath = path.resolve('src/middleware.js')
398
+ export async function build (cliArgs = []) {
399
+ const options = arg({
400
+ '--config': String,
401
+ '-c': '--config',
402
+ }, { argv: cliArgs })
925
403
 
926
- if (!fs.existsSync(middlewareSourcePath)) {
927
- middlewareSourcePath = path.resolve(scriptDir, 'middleware.js')
928
- }
404
+ const scriptDir = path.dirname(url.fileURLToPath(import.meta.url))
405
+ const cwd = process.cwd()
406
+ const buildPath = path.resolve(cwd, 'build')
407
+ const srcDir = path.resolve(cwd, 'src')
408
+ const publicDir = path.resolve(cwd, 'public')
409
+ const configPath = path.resolve(cwd, options['--config'] || 'src/config.js')
929
410
 
930
- const configSourcePath = fs.existsSync(configPath)
931
- ? configPath
932
- : path.resolve(scriptDir, 'default-config.js')
411
+ resetBuildDir(buildPath)
412
+ writeResourceTemplatesBarrelIfPresent({ cwd, log: false })
413
+ const ossyDir = ossyGeneratedDir(buildPath)
933
414
 
934
- console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild\x1b[0m')
935
- printBuildOverview({
936
- pagesSourcePath: pagesGeneratedPath,
937
- apiOverviewFiles,
938
- configPath,
939
- pageFiles,
940
- })
941
-
942
- if (resourceTemplatesResult.wrote && resourceTemplatesResult.path) {
943
- console.log(
944
- `[@ossy/app][resource-templates] merged ${resourceTemplatesResult.count} template(s) → ${path.relative(process.cwd(), resourceTemplatesResult.path)}`
945
- )
946
- }
947
-
948
- const { taskOverviewFiles } = resolveTaskSource({ srcDir, buildPath })
949
- await compileOssyNodeArtifacts({
950
- apiFiles: apiOverviewFiles,
951
- taskFiles: taskOverviewFiles,
952
- ossyDir,
953
- nodeEnv: 'production',
954
- })
415
+ const pageFiles = discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN)
416
+ const apiFiles = discoverFilesByPattern(srcDir, API_FILE_PATTERN)
417
+ const taskFiles = discoverFilesByPattern(srcDir, TASK_FILE_PATTERN)
955
418
 
956
- writeAppRuntimeShims({
957
- middlewareSourcePath,
958
- configSourcePath,
959
- ossyDir,
960
- })
961
- copyOssyAppRuntime({ scriptDir, buildPath })
962
-
963
- await compileCombinedBundle({
964
- ssrEntryPath,
965
- clientEntryPath,
966
- buildPath,
967
- nodeEnv: 'production',
968
- copyPublicFrom: publicDir,
419
+ writeJson(
420
+ path.join(ossyDir, OSSY_GEN_PAGES_BASENAME),
421
+ pageFiles.map((f) => {
422
+ const { id, path: routePath } = pageRouteFromSource(f, srcDir)
423
+ return { id, path: routePath, sourceFile: path.relative(cwd, f).replace(/\\/g, '/') }
969
424
  })
425
+ )
970
426
 
971
- console.log(' \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild finished\x1b[0m\n')
972
- };
427
+ const apiRouteList = await compileApiServerModules({ apiFiles, ossyDir, nodeEnv: 'production' })
428
+ const taskRouteList = await compileTaskServerModules({ taskFiles, ossyDir, nodeEnv: 'production' })
429
+ writeJson(path.join(ossyDir, OSSY_GEN_API_BASENAME), apiRouteList)
430
+ writeJson(path.join(ossyDir, OSSY_GEN_TASKS_BASENAME), taskRouteList)
431
+
432
+ const middlewareSourcePath = fs.existsSync(path.resolve(cwd, 'src/middleware.js'))
433
+ ? path.resolve(cwd, 'src/middleware.js')
434
+ : path.resolve(scriptDir, 'Middleware.js')
435
+ const configSourcePath = fs.existsSync(configPath)
436
+ ? configPath
437
+ : path.resolve(scriptDir, 'default-config.js')
438
+ writeAppRuntimeShims({ middlewareSourcePath, configSourcePath, ossyDir })
439
+
440
+ const { ssrEntryPath, hydrateEntryPath } = writeGeneratedEntries({ pageFiles, srcDir, ossyDir })
441
+ copyPlatformRuntime({ buildPath })
442
+ await compileCombinedBundle({
443
+ ssrEntryPath,
444
+ clientEntryPath: hydrateEntryPath,
445
+ buildPath,
446
+ nodeEnv: 'production',
447
+ copyPublicFrom: publicDir,
448
+ })
449
+ }