@ossy/app 1.11.1 → 1.11.3
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 +3 -3
- package/cli/api.runtime.mjs +22 -0
- package/cli/build.js +280 -166
- package/cli/dev.js +76 -100
- package/cli/pages.runtime.mjs +39 -0
- package/cli/prerender-react.task.js +6 -35
- package/cli/render-page.task.js +32 -0
- package/cli/server.js +73 -11
- package/cli/tasks.runtime.mjs +22 -0
- package/cli/worker-entry.js +1 -1
- package/package.json +10 -10
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
|
-
|
|
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
|
|
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 **`
|
|
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,37 @@ 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
|
-
|
|
57
|
-
export const
|
|
58
|
-
export const
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
export const
|
|
62
|
-
export const
|
|
63
|
-
export const
|
|
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
|
+
/** Tiny Rollup inputs that re-export `metadata` so per-page server bundles keep i18n paths. */
|
|
71
|
+
export const OSSY_PAGE_SERVER_ENTRIES_DIRNAME = 'page-server-entries'
|
|
72
|
+
export const OSSY_API_MODULES_DIRNAME = 'api-modules'
|
|
73
|
+
export const OSSY_TASK_MODULES_DIRNAME = 'task-modules'
|
|
74
|
+
|
|
64
75
|
export const OSSY_MIDDLEWARE_RUNTIME_BASENAME = 'middleware.runtime.js'
|
|
76
|
+
export const OSSY_SERVER_CONFIG_RUNTIME_BASENAME = 'server-config.runtime.mjs'
|
|
77
|
+
export const OSSY_RENDER_PAGE_RUNTIME_BASENAME = 'render-page.task.js'
|
|
78
|
+
|
|
79
|
+
/** Keep React external across per-page server chunks so `pages.runtime.mjs` shares one React. */
|
|
80
|
+
export const OSSY_PAGE_SERVER_EXTERNAL = [
|
|
81
|
+
'react',
|
|
82
|
+
'react-dom',
|
|
83
|
+
'react-dom/static',
|
|
84
|
+
'react-dom/client',
|
|
85
|
+
'react/jsx-runtime',
|
|
86
|
+
]
|
|
65
87
|
|
|
66
88
|
/** Per-page client entries: `hydrate-<pageId>.jsx` under `.ossy/` */
|
|
67
89
|
const HYDRATE_STUB_PREFIX = 'hydrate-'
|
|
@@ -210,9 +232,10 @@ export function createOssyClientRollupPlugins ({ nodeEnv, copyPublicFrom, buildP
|
|
|
210
232
|
}
|
|
211
233
|
|
|
212
234
|
/** Bundles a single Node ESM file (inline dynamic imports) for SSR / API / tasks. */
|
|
213
|
-
export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv, onWarn }) {
|
|
235
|
+
export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv, onWarn, external }) {
|
|
214
236
|
const bundle = await rollup({
|
|
215
237
|
input: inputPath,
|
|
238
|
+
...(external && external.length ? { external } : {}),
|
|
216
239
|
plugins: createOssyAppBundlePlugins({ nodeEnv }),
|
|
217
240
|
onwarn (warning, defaultHandler) {
|
|
218
241
|
if (onWarn) {
|
|
@@ -231,14 +254,19 @@ export async function bundleOssyNodeEntry ({ inputPath, outputFile, nodeEnv, onW
|
|
|
231
254
|
}
|
|
232
255
|
|
|
233
256
|
/**
|
|
234
|
-
* Re-exports middleware via `file:`
|
|
257
|
+
* Re-exports middleware and app config via `file:` URLs so `src/*.js` can keep relative imports.
|
|
235
258
|
*/
|
|
236
|
-
export function writeAppRuntimeShims ({ middlewareSourcePath, ossyDir }) {
|
|
259
|
+
export function writeAppRuntimeShims ({ middlewareSourcePath, configSourcePath, ossyDir }) {
|
|
237
260
|
const mwHref = url.pathToFileURL(path.resolve(middlewareSourcePath)).href
|
|
238
261
|
fs.writeFileSync(
|
|
239
262
|
path.join(ossyDir, OSSY_MIDDLEWARE_RUNTIME_BASENAME),
|
|
240
263
|
`// Generated by @ossy/app — do not edit\nexport { default } from '${mwHref}'\n`
|
|
241
264
|
)
|
|
265
|
+
const cfgHref = url.pathToFileURL(path.resolve(configSourcePath)).href
|
|
266
|
+
fs.writeFileSync(
|
|
267
|
+
path.join(ossyDir, OSSY_SERVER_CONFIG_RUNTIME_BASENAME),
|
|
268
|
+
`// Generated by @ossy/app — do not edit\nexport { default } from '${cfgHref}'\n`
|
|
269
|
+
)
|
|
242
270
|
}
|
|
243
271
|
|
|
244
272
|
/**
|
|
@@ -258,6 +286,18 @@ export function copyOssyAppRuntime ({ scriptDir, buildPath }) {
|
|
|
258
286
|
}
|
|
259
287
|
fs.copyFileSync(path.join(scriptDir, 'worker-entry.js'), path.join(buildPath, 'worker.js'))
|
|
260
288
|
fs.copyFileSync(path.join(scriptDir, 'worker-runtime.js'), path.join(buildPath, 'worker-runtime.js'))
|
|
289
|
+
const ossyOut = ossyGeneratedDir(buildPath)
|
|
290
|
+
for (const name of [
|
|
291
|
+
OSSY_PAGES_RUNTIME_BASENAME,
|
|
292
|
+
OSSY_API_RUNTIME_BASENAME,
|
|
293
|
+
OSSY_TASKS_RUNTIME_BASENAME,
|
|
294
|
+
]) {
|
|
295
|
+
fs.copyFileSync(path.join(scriptDir, name), path.join(ossyOut, name))
|
|
296
|
+
}
|
|
297
|
+
fs.copyFileSync(
|
|
298
|
+
path.join(scriptDir, OSSY_RENDER_PAGE_RUNTIME_BASENAME),
|
|
299
|
+
path.join(ossyOut, OSSY_RENDER_PAGE_RUNTIME_BASENAME)
|
|
300
|
+
)
|
|
261
301
|
}
|
|
262
302
|
|
|
263
303
|
/**
|
|
@@ -281,107 +321,162 @@ export function discoverFilesByPattern (srcDir, filePattern) {
|
|
|
281
321
|
return files.sort()
|
|
282
322
|
}
|
|
283
323
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
324
|
+
export function writeOssyJson (filePath, data) {
|
|
325
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
326
|
+
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** JSON manifest: discovered API source paths (posix, relative to `cwd`). */
|
|
330
|
+
export function buildApiManifestPayload (apiFiles, cwd = process.cwd()) {
|
|
331
|
+
return {
|
|
332
|
+
version: 1,
|
|
333
|
+
files: apiFiles.map((f) => path.relative(cwd, f).replace(/\\/g, '/')),
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** JSON manifest: discovered task source paths (posix, relative to `cwd`). */
|
|
338
|
+
export function buildTasksManifestPayload (taskFiles, cwd = process.cwd()) {
|
|
339
|
+
return {
|
|
340
|
+
version: 1,
|
|
341
|
+
files: taskFiles.map((f) => path.relative(cwd, f).replace(/\\/g, '/')),
|
|
296
342
|
}
|
|
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
343
|
}
|
|
321
344
|
|
|
322
345
|
/**
|
|
323
|
-
* Writes `build/.ossy/api.generated.
|
|
324
|
-
*
|
|
346
|
+
* Writes `build/.ossy/api.generated.json` (sources only).
|
|
347
|
+
* Compiled modules + `api.bundle.json` are produced by {@link compileOssyNodeArtifacts}.
|
|
325
348
|
*/
|
|
326
|
-
export function resolveApiSource ({ srcDir, buildPath }) {
|
|
349
|
+
export function resolveApiSource ({ srcDir, buildPath, cwd = process.cwd() }) {
|
|
327
350
|
ensureOssyGeneratedDir(buildPath)
|
|
328
351
|
const generatedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_API_BASENAME)
|
|
329
352
|
const apiFiles = discoverFilesByPattern(srcDir, API_FILE_PATTERN)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
generateApiModule({ generatedPath, apiFiles })
|
|
333
|
-
)
|
|
334
|
-
return { apiSourcePath: generatedPath, apiOverviewFiles: apiFiles }
|
|
353
|
+
writeOssyJson(generatedPath, buildApiManifestPayload(apiFiles, cwd))
|
|
354
|
+
return { apiGeneratedPath: generatedPath, apiOverviewFiles: apiFiles }
|
|
335
355
|
}
|
|
336
356
|
|
|
337
357
|
/**
|
|
338
|
-
*
|
|
339
|
-
* of job handlers `{ type, handler }` for the Ossy worker.
|
|
358
|
+
* Writes `build/.ossy/tasks.generated.json` (sources only).
|
|
340
359
|
*/
|
|
341
|
-
export function
|
|
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 }) {
|
|
360
|
+
export function resolveTaskSource ({ srcDir, buildPath, cwd = process.cwd() }) {
|
|
374
361
|
ensureOssyGeneratedDir(buildPath)
|
|
375
362
|
const generatedPath = path.join(ossyGeneratedDir(buildPath), OSSY_GEN_TASKS_BASENAME)
|
|
376
363
|
const taskFiles = discoverFilesByPattern(srcDir, TASK_FILE_PATTERN)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
364
|
+
writeOssyJson(generatedPath, buildTasksManifestPayload(taskFiles, cwd))
|
|
365
|
+
return { tasksGeneratedPath: generatedPath, taskOverviewFiles: taskFiles }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function compilePageServerModules ({
|
|
369
|
+
pageFiles,
|
|
370
|
+
srcDir,
|
|
371
|
+
ossyDir,
|
|
372
|
+
nodeEnv,
|
|
373
|
+
onWarn,
|
|
374
|
+
}) {
|
|
375
|
+
const modsDir = path.join(ossyDir, OSSY_PAGE_MODULES_DIRNAME)
|
|
376
|
+
const entriesDir = path.join(ossyDir, OSSY_PAGE_SERVER_ENTRIES_DIRNAME)
|
|
377
|
+
fs.rmSync(modsDir, { recursive: true, force: true })
|
|
378
|
+
fs.rmSync(entriesDir, { recursive: true, force: true })
|
|
379
|
+
if (pageFiles.length === 0) {
|
|
380
|
+
return []
|
|
381
|
+
}
|
|
382
|
+
fs.mkdirSync(modsDir, { recursive: true })
|
|
383
|
+
fs.mkdirSync(entriesDir, { recursive: true })
|
|
384
|
+
const bundlePages = []
|
|
385
|
+
for (const f of pageFiles) {
|
|
386
|
+
const pageId = clientHydrateIdForPage(f, srcDir)
|
|
387
|
+
const safeId = String(pageId).replace(/[^a-zA-Z0-9_-]+/g, '-') || 'page'
|
|
388
|
+
const outName = `${safeId}.mjs`
|
|
389
|
+
const outFile = path.join(modsDir, outName)
|
|
390
|
+
const stubPath = path.join(entriesDir, `${safeId}.mjs`)
|
|
391
|
+
writePageServerRollupEntry({ pageAbsPath: f, stubPath })
|
|
392
|
+
await bundleOssyNodeEntry({
|
|
393
|
+
inputPath: stubPath,
|
|
394
|
+
outputFile: outFile,
|
|
395
|
+
nodeEnv,
|
|
396
|
+
onWarn,
|
|
397
|
+
external: OSSY_PAGE_SERVER_EXTERNAL,
|
|
382
398
|
})
|
|
383
|
-
|
|
384
|
-
|
|
399
|
+
bundlePages.push({
|
|
400
|
+
id: pageId,
|
|
401
|
+
module: `${OSSY_PAGE_MODULES_DIRNAME}/${outName}`,
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
return bundlePages
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export async function compileApiServerModules ({ apiFiles, ossyDir, nodeEnv, onWarn }) {
|
|
408
|
+
const modsDir = path.join(ossyDir, OSSY_API_MODULES_DIRNAME)
|
|
409
|
+
fs.rmSync(modsDir, { recursive: true, force: true })
|
|
410
|
+
if (apiFiles.length === 0) {
|
|
411
|
+
return []
|
|
412
|
+
}
|
|
413
|
+
fs.mkdirSync(modsDir, { recursive: true })
|
|
414
|
+
const modules = []
|
|
415
|
+
for (let i = 0; i < apiFiles.length; i++) {
|
|
416
|
+
const outName = `api-${i}.mjs`
|
|
417
|
+
const outFile = path.join(modsDir, outName)
|
|
418
|
+
await bundleOssyNodeEntry({
|
|
419
|
+
inputPath: apiFiles[i],
|
|
420
|
+
outputFile: outFile,
|
|
421
|
+
nodeEnv,
|
|
422
|
+
onWarn,
|
|
423
|
+
})
|
|
424
|
+
modules.push(`${OSSY_API_MODULES_DIRNAME}/${outName}`)
|
|
425
|
+
}
|
|
426
|
+
return modules
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export async function compileTaskServerModules ({ taskFiles, ossyDir, nodeEnv, onWarn }) {
|
|
430
|
+
const modsDir = path.join(ossyDir, OSSY_TASK_MODULES_DIRNAME)
|
|
431
|
+
fs.rmSync(modsDir, { recursive: true, force: true })
|
|
432
|
+
if (taskFiles.length === 0) {
|
|
433
|
+
return []
|
|
434
|
+
}
|
|
435
|
+
fs.mkdirSync(modsDir, { recursive: true })
|
|
436
|
+
const modules = []
|
|
437
|
+
for (let i = 0; i < taskFiles.length; i++) {
|
|
438
|
+
const outName = `task-${i}.mjs`
|
|
439
|
+
const outFile = path.join(modsDir, outName)
|
|
440
|
+
await bundleOssyNodeEntry({
|
|
441
|
+
inputPath: taskFiles[i],
|
|
442
|
+
outputFile: outFile,
|
|
443
|
+
nodeEnv,
|
|
444
|
+
onWarn,
|
|
445
|
+
})
|
|
446
|
+
modules.push(`${OSSY_TASK_MODULES_DIRNAME}/${outName}`)
|
|
447
|
+
}
|
|
448
|
+
return modules
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Writes `pages.bundle.json`, `api.bundle.json`, `tasks.bundle.json` by Rollup-compiling each source module.
|
|
453
|
+
*/
|
|
454
|
+
export async function compileOssyNodeArtifacts ({
|
|
455
|
+
pageFiles,
|
|
456
|
+
srcDir,
|
|
457
|
+
ossyDir,
|
|
458
|
+
apiFiles,
|
|
459
|
+
taskFiles,
|
|
460
|
+
nodeEnv,
|
|
461
|
+
onWarn,
|
|
462
|
+
}) {
|
|
463
|
+
const [pageBundleList, apiModuleList, taskModuleList] = await Promise.all([
|
|
464
|
+
compilePageServerModules({ pageFiles, srcDir, ossyDir, nodeEnv, onWarn }),
|
|
465
|
+
compileApiServerModules({ apiFiles, ossyDir, nodeEnv, onWarn }),
|
|
466
|
+
compileTaskServerModules({ taskFiles, ossyDir, nodeEnv, onWarn }),
|
|
467
|
+
])
|
|
468
|
+
writeOssyJson(path.join(ossyDir, OSSY_PAGES_BUNDLE_BASENAME), {
|
|
469
|
+
version: 1,
|
|
470
|
+
pages: pageBundleList,
|
|
471
|
+
})
|
|
472
|
+
writeOssyJson(path.join(ossyDir, OSSY_API_BUNDLE_BASENAME), {
|
|
473
|
+
version: 1,
|
|
474
|
+
modules: apiModuleList,
|
|
475
|
+
})
|
|
476
|
+
writeOssyJson(path.join(ossyDir, OSSY_TASKS_BUNDLE_BASENAME), {
|
|
477
|
+
version: 1,
|
|
478
|
+
modules: taskModuleList,
|
|
479
|
+
})
|
|
385
480
|
}
|
|
386
481
|
|
|
387
482
|
export function filePathToRoute(filePath, srcDir) {
|
|
@@ -412,9 +507,40 @@ export function clientHydrateIdForPage (pageAbsPath, srcDir) {
|
|
|
412
507
|
return idMatch ? idMatch[1] : derived.id
|
|
413
508
|
}
|
|
414
509
|
|
|
510
|
+
export function pageSourceExportsMetadata (pageAbsPath) {
|
|
511
|
+
try {
|
|
512
|
+
const src = fs.readFileSync(pageAbsPath, 'utf8')
|
|
513
|
+
return /\bexport\s+const\s+metadata\b/.test(src)
|
|
514
|
+
} catch {
|
|
515
|
+
return false
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Rollup tree-shakes `export const metadata` when the page file is the entry and `default`
|
|
521
|
+
* never references it — breaks i18n `path` objects at runtime. Re-export from a shim entry
|
|
522
|
+
* so `metadata` stays in the module graph.
|
|
523
|
+
*/
|
|
524
|
+
export function writePageServerRollupEntry ({ pageAbsPath, stubPath }) {
|
|
525
|
+
const rel = relToGeneratedImport(stubPath, pageAbsPath)
|
|
526
|
+
const meta = pageSourceExportsMetadata(pageAbsPath)
|
|
527
|
+
? ', metadata'
|
|
528
|
+
: ''
|
|
529
|
+
fs.mkdirSync(path.dirname(stubPath), { recursive: true })
|
|
530
|
+
fs.writeFileSync(
|
|
531
|
+
stubPath,
|
|
532
|
+
[
|
|
533
|
+
'// Generated by @ossy/app — do not edit',
|
|
534
|
+
`export { default${meta} } from '${rel}'`,
|
|
535
|
+
'',
|
|
536
|
+
].join('\n'),
|
|
537
|
+
'utf8'
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
|
|
415
541
|
/**
|
|
416
542
|
* One client entry per page: imports only that page module and hydrates the document.
|
|
417
|
-
* Keeps the same `toPage` shape as `
|
|
543
|
+
* Keeps the same `toPage` shape as `pages.runtime.mjs` + manifests so SSR and client trees match.
|
|
418
544
|
*/
|
|
419
545
|
export function generatePageHydrateModule ({ pageAbsPath, stubAbsPath, srcDir }) {
|
|
420
546
|
const rel = relToGeneratedImport(stubAbsPath, pageAbsPath)
|
|
@@ -491,32 +617,39 @@ export function buildClientHydrateInput (pageFiles, srcDir, ossyDir) {
|
|
|
491
617
|
return input
|
|
492
618
|
}
|
|
493
619
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
620
|
+
/** JSON manifest: route ids, default paths, and page source paths (posix, relative to `cwd`). */
|
|
621
|
+
export function buildPagesGeneratedPayload (pageFiles, srcDir, cwd = process.cwd()) {
|
|
622
|
+
const pages = pageFiles.map((f) => {
|
|
623
|
+
const { path: routePath } = filePathToRoute(f, srcDir)
|
|
624
|
+
const pageId = clientHydrateIdForPage(f, srcDir)
|
|
625
|
+
return {
|
|
626
|
+
id: pageId,
|
|
627
|
+
path: routePath,
|
|
628
|
+
sourceFile: path.relative(cwd, f).replace(/\\/g, '/'),
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
return { version: 1, pages }
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export function writePagesManifest ({
|
|
635
|
+
pageFiles,
|
|
636
|
+
srcDir,
|
|
637
|
+
pagesGeneratedPath,
|
|
638
|
+
cwd = process.cwd(),
|
|
639
|
+
}) {
|
|
640
|
+
writeOssyJson(pagesGeneratedPath, buildPagesGeneratedPayload(pageFiles, srcDir, cwd))
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function parsePagesFromManifestJson (manifestPath) {
|
|
644
|
+
try {
|
|
645
|
+
const raw = fs.readFileSync(manifestPath, 'utf8')
|
|
646
|
+
const data = JSON.parse(raw)
|
|
647
|
+
const pages = data?.pages
|
|
648
|
+
if (!Array.isArray(pages)) return []
|
|
649
|
+
return pages.map((p) => ({ id: p.id, path: p.path }))
|
|
650
|
+
} catch {
|
|
651
|
+
return []
|
|
652
|
+
}
|
|
520
653
|
}
|
|
521
654
|
|
|
522
655
|
export function parsePagesFromSource(filePath) {
|
|
@@ -557,7 +690,6 @@ export function parsePagesFromSource(filePath) {
|
|
|
557
690
|
*/
|
|
558
691
|
export function getBuildOverviewSnapshot ({
|
|
559
692
|
pagesSourcePath,
|
|
560
|
-
apiSourcePath,
|
|
561
693
|
apiOverviewFiles = [],
|
|
562
694
|
configPath,
|
|
563
695
|
pageFiles,
|
|
@@ -567,17 +699,14 @@ export function getBuildOverviewSnapshot ({
|
|
|
567
699
|
const configRel = fs.existsSync(configPath) ? rel(configPath) : null
|
|
568
700
|
|
|
569
701
|
const pages = pageFiles?.length
|
|
570
|
-
? pageFiles.map((f) =>
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
: fs.existsSync(apiSourcePath)
|
|
577
|
-
? [apiSourcePath]
|
|
578
|
-
: []
|
|
702
|
+
? pageFiles.map((f) => {
|
|
703
|
+
const { id, path: routePath } = filePathToRoute(f, srcDir)
|
|
704
|
+
return { id: clientHydrateIdForPage(f, srcDir), path: routePath }
|
|
705
|
+
})
|
|
706
|
+
: parsePagesFromManifestJson(pagesSourcePath)
|
|
707
|
+
|
|
579
708
|
const apiRoutes = []
|
|
580
|
-
for (const f of
|
|
709
|
+
for (const f of apiOverviewFiles) {
|
|
581
710
|
if (fs.existsSync(f)) apiRoutes.push(...parsePagesFromSource(f))
|
|
582
711
|
}
|
|
583
712
|
|
|
@@ -633,24 +762,21 @@ export const build = async (cliArgs) => {
|
|
|
633
762
|
log: false,
|
|
634
763
|
})
|
|
635
764
|
|
|
636
|
-
const
|
|
765
|
+
const ossyDir = ossyGeneratedDir(buildPath)
|
|
766
|
+
const pagesGeneratedPath = path.join(ossyDir, OSSY_GEN_PAGES_BASENAME)
|
|
637
767
|
|
|
638
|
-
|
|
768
|
+
writePagesManifest({
|
|
769
|
+
pageFiles,
|
|
770
|
+
srcDir,
|
|
639
771
|
pagesGeneratedPath,
|
|
640
|
-
|
|
641
|
-
)
|
|
642
|
-
const ossyDir = ossyGeneratedDir(buildPath)
|
|
772
|
+
})
|
|
643
773
|
writePageHydrateStubs(pageFiles, srcDir, ossyDir)
|
|
644
774
|
const clientHydrateInput = buildClientHydrateInput(pageFiles, srcDir, ossyDir)
|
|
645
775
|
|
|
646
|
-
const {
|
|
647
|
-
apiSourcePath: resolvedApi,
|
|
648
|
-
apiOverviewFiles,
|
|
649
|
-
} = resolveApiSource({
|
|
776
|
+
const { apiOverviewFiles } = resolveApiSource({
|
|
650
777
|
srcDir,
|
|
651
778
|
buildPath,
|
|
652
779
|
})
|
|
653
|
-
let apiSourcePath = resolvedApi
|
|
654
780
|
let middlewareSourcePath = path.resolve('src/middleware.js');
|
|
655
781
|
const publicDir = path.resolve('public')
|
|
656
782
|
|
|
@@ -662,14 +788,11 @@ export const build = async (cliArgs) => {
|
|
|
662
788
|
? configPath
|
|
663
789
|
: path.resolve(scriptDir, 'default-config.js')
|
|
664
790
|
|
|
665
|
-
const
|
|
666
|
-
const apiBundlePath = path.join(ossyDir, OSSY_API_SERVER_BUNDLE)
|
|
667
|
-
const tasksBundlePath = path.join(ossyDir, OSSY_TASKS_SERVER_BUNDLE)
|
|
791
|
+
const pagesEntryPath = path.join(ossyDir, OSSY_PAGES_RUNTIME_BASENAME)
|
|
668
792
|
|
|
669
793
|
const useDashboard = Object.keys(clientHydrateInput).length > 0
|
|
670
794
|
const overviewSnap = getBuildOverviewSnapshot({
|
|
671
795
|
pagesSourcePath: pagesGeneratedPath,
|
|
672
|
-
apiSourcePath,
|
|
673
796
|
apiOverviewFiles,
|
|
674
797
|
configPath,
|
|
675
798
|
pageFiles,
|
|
@@ -706,7 +829,6 @@ export const build = async (cliArgs) => {
|
|
|
706
829
|
console.log('\n \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild\x1b[0m')
|
|
707
830
|
printBuildOverview({
|
|
708
831
|
pagesSourcePath: pagesGeneratedPath,
|
|
709
|
-
apiSourcePath,
|
|
710
832
|
apiOverviewFiles,
|
|
711
833
|
configPath,
|
|
712
834
|
pageFiles,
|
|
@@ -724,28 +846,20 @@ export const build = async (cliArgs) => {
|
|
|
724
846
|
}
|
|
725
847
|
: undefined
|
|
726
848
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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,
|
|
849
|
+
const { taskOverviewFiles } = resolveTaskSource({ srcDir, buildPath })
|
|
850
|
+
await compileOssyNodeArtifacts({
|
|
851
|
+
pageFiles,
|
|
852
|
+
srcDir,
|
|
853
|
+
ossyDir,
|
|
854
|
+
apiFiles: apiOverviewFiles,
|
|
855
|
+
taskFiles: taskOverviewFiles,
|
|
743
856
|
nodeEnv: 'production',
|
|
744
857
|
onWarn: warnSink,
|
|
745
858
|
})
|
|
746
859
|
|
|
747
860
|
writeAppRuntimeShims({
|
|
748
861
|
middlewareSourcePath,
|
|
862
|
+
configSourcePath,
|
|
749
863
|
ossyDir,
|
|
750
864
|
})
|
|
751
865
|
copyOssyAppRuntime({ scriptDir, buildPath })
|
|
@@ -759,7 +873,7 @@ export const build = async (cliArgs) => {
|
|
|
759
873
|
copyPublicFrom: publicDir,
|
|
760
874
|
buildPath,
|
|
761
875
|
nodeEnv: 'production',
|
|
762
|
-
|
|
876
|
+
pagesEntryPath,
|
|
763
877
|
configSourcePath,
|
|
764
878
|
createClientRollupPlugins: createOssyClientRollupPlugins,
|
|
765
879
|
minifyBrowserStaticChunks,
|
package/cli/dev.js
CHANGED
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
filePathToRoute,
|
|
8
8
|
discoverFilesByPattern,
|
|
9
9
|
PAGE_FILE_PATTERN,
|
|
10
|
-
|
|
10
|
+
writePagesManifest,
|
|
11
11
|
resolveApiSource,
|
|
12
12
|
resolveTaskSource,
|
|
13
13
|
resetOssyBuildDir,
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
62
|
+
pagesGeneratedPath,
|
|
76
63
|
})
|
|
77
|
-
|
|
78
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 (
|
|
237
|
+
if (currentPageFiles.length > 0) {
|
|
258
238
|
const pageIds = [
|
|
259
|
-
...new Set(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
+
writePagesManifest({
|
|
311
|
+
pageFiles: refreshedPageFiles,
|
|
312
|
+
srcDir,
|
|
313
|
+
pagesGeneratedPath: regenPath,
|
|
314
|
+
})
|
|
341
315
|
writePageHydrateStubs(refreshedPageFiles, srcDir, ossyDir)
|
|
342
|
-
|
|
343
|
-
watcher
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
+
pagesEntryPath: input.pagesEntryPath,
|
|
288
259
|
configSourcePath: input.configSourcePath,
|
|
289
260
|
publicDir: input.publicDir,
|
|
290
261
|
reporter: input.reporter,
|
package/cli/render-page.task.js
CHANGED
|
@@ -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,34 @@ 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.
|
|
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
|
+
|
|
20
|
+
/** When `src/config.js` is minimal, infer language list from the first multi-path page. */
|
|
21
|
+
function pageRouterLanguageOptions (config, pages) {
|
|
22
|
+
let supported = config?.supportedLanguages
|
|
23
|
+
let defaultLanguage = config?.defaultLanguage
|
|
24
|
+
if ((!supported || supported.length <= 1) && pages.length > 0) {
|
|
25
|
+
const p0 = pages[0]
|
|
26
|
+
if (p0 && typeof p0.path === 'object' && p0.path != null) {
|
|
27
|
+
supported = Object.keys(p0.path)
|
|
28
|
+
defaultLanguage = defaultLanguage || supported[0]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
supportedLanguages: Array.isArray(supported) ? supported : [],
|
|
33
|
+
defaultLanguage,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
const app = express();
|
|
16
38
|
|
|
17
39
|
const currentDir = path.dirname(url.fileURLToPath(import.meta.url))
|
|
@@ -101,22 +123,62 @@ const middleware = [
|
|
|
101
123
|
|
|
102
124
|
app.use(middleware)
|
|
103
125
|
|
|
104
|
-
const
|
|
126
|
+
const apiRouter = OssyRouter.of({
|
|
105
127
|
pages: apiRouteList,
|
|
106
128
|
})
|
|
107
129
|
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
const { supportedLanguages, defaultLanguage } = pageRouterLanguageOptions(
|
|
131
|
+
buildTimeConfig,
|
|
132
|
+
sitePageList
|
|
133
|
+
)
|
|
110
134
|
|
|
111
|
-
|
|
135
|
+
const pageRouter = OssyRouter.of({
|
|
136
|
+
pages: sitePageList,
|
|
137
|
+
defaultLanguage,
|
|
138
|
+
supportedLanguages,
|
|
139
|
+
})
|
|
112
140
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
route.handle(req, res)
|
|
116
|
-
return
|
|
117
|
-
}
|
|
141
|
+
app.all('*all', async (req, res) => {
|
|
142
|
+
const requestUrl = req.originalUrl || '/'
|
|
118
143
|
|
|
119
|
-
|
|
144
|
+
try {
|
|
145
|
+
const apiRoute = apiRouter.getPageByUrl(requestUrl)
|
|
146
|
+
if (apiRoute && typeof apiRoute.handle === 'function') {
|
|
147
|
+
console.log(`[@ossy/app][server] Handling API route: ${requestUrl}`)
|
|
148
|
+
apiRoute.handle(req, res)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
153
|
+
res.status(404).send('Not found')
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const pageRoute = pageRouter.getPageByUrl(requestUrl)
|
|
158
|
+
if (pageRoute?.element) {
|
|
159
|
+
const appConfig = buildPrerenderAppConfig({
|
|
160
|
+
buildTimeConfig,
|
|
161
|
+
pageList: sitePageList,
|
|
162
|
+
activeRouteId: pageRoute.id,
|
|
163
|
+
urlPath: requestUrl,
|
|
164
|
+
isAuthenticated: !!req.isAuthenticated,
|
|
165
|
+
})
|
|
166
|
+
const html = await BuildPage.handle({
|
|
167
|
+
route: pageRoute,
|
|
168
|
+
appConfig,
|
|
169
|
+
isDevReloadEnabled,
|
|
170
|
+
})
|
|
171
|
+
res.status(200).type('html').send(html)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
res.status(404).send('Not found')
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error('[@ossy/app][server] Request handling failed:', err)
|
|
178
|
+
if (!res.headersSent) {
|
|
179
|
+
res.status(500).type('text').send('Internal Server Error')
|
|
180
|
+
}
|
|
181
|
+
}
|
|
120
182
|
})
|
|
121
183
|
|
|
122
184
|
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
|
package/cli/worker-entry.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/app",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.3",
|
|
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.
|
|
31
|
-
"@ossy/design-system": "^1.11.
|
|
32
|
-
"@ossy/pages": "^1.11.
|
|
33
|
-
"@ossy/router": "^1.11.
|
|
34
|
-
"@ossy/router-react": "^1.11.
|
|
35
|
-
"@ossy/sdk": "^1.11.
|
|
36
|
-
"@ossy/sdk-react": "^1.11.
|
|
37
|
-
"@ossy/themes": "^1.11.
|
|
30
|
+
"@ossy/connected-components": "^1.11.3",
|
|
31
|
+
"@ossy/design-system": "^1.11.3",
|
|
32
|
+
"@ossy/pages": "^1.11.3",
|
|
33
|
+
"@ossy/router": "^1.11.3",
|
|
34
|
+
"@ossy/router-react": "^1.11.3",
|
|
35
|
+
"@ossy/sdk": "^1.11.3",
|
|
36
|
+
"@ossy/sdk-react": "^1.11.3",
|
|
37
|
+
"@ossy/themes": "^1.11.3",
|
|
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": "
|
|
70
|
+
"gitHead": "c8b8fe48b3462ef9f7994894a1b17d844740dc11"
|
|
71
71
|
}
|