@ossy/app 1.11.0 → 1.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/cli/api.runtime.mjs +22 -0
- package/cli/build-terminal.js +170 -37
- package/cli/build.js +335 -193
- package/cli/dev.js +129 -109
- 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 +51 -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-terminal.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import util from 'node:util'
|
|
2
|
+
|
|
1
3
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
2
4
|
|
|
3
5
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`
|
|
@@ -15,6 +17,13 @@ function padVisible (s, width) {
|
|
|
15
17
|
return n >= width ? s : s + ' '.repeat(width - n)
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
function truncateVisible (s, maxW) {
|
|
21
|
+
const plain = stripAnsi(s)
|
|
22
|
+
if (plain.length <= maxW) return s
|
|
23
|
+
const cut = Math.max(0, maxW - 1)
|
|
24
|
+
return plain.slice(0, cut) + '…'
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
export function pageIdFromHydrateEntryName (entryName) {
|
|
19
28
|
const p = String(entryName)
|
|
20
29
|
return p.startsWith('hydrate__') ? p.slice('hydrate__'.length) : p
|
|
@@ -51,24 +60,35 @@ function rowLeadIcon (r, mode) {
|
|
|
51
60
|
return green('✔')
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
function useTty (stream) {
|
|
64
|
+
return (
|
|
65
|
+
stream.isTTY === true &&
|
|
66
|
+
process.env.TERM !== 'dumb' &&
|
|
67
|
+
process.env.CI !== 'true' &&
|
|
68
|
+
process.env.OSSY_PLAIN_OUTPUT !== '1'
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
/**
|
|
55
|
-
*
|
|
56
|
-
* TTY:
|
|
73
|
+
* Merged overview + per-page progress, split layout: left = status table, right = log tail.
|
|
74
|
+
* TTY: single redraw block + optional console capture into the right column.
|
|
57
75
|
*/
|
|
58
|
-
export function
|
|
76
|
+
export function createBuildDashboard ({
|
|
59
77
|
scope = '@ossy/app',
|
|
78
|
+
mode = 'full',
|
|
60
79
|
pageIds,
|
|
80
|
+
/** @type {Record<string, string>} route path per page id */
|
|
81
|
+
idToPath = {},
|
|
82
|
+
overview = { title: '@ossy/app', configRel: null, apiRoutes: [] },
|
|
61
83
|
stream = process.stdout,
|
|
62
|
-
/** `'full'`: hydrate + prerender columns. `'prerender-only'`: dev watch (client built separately). */
|
|
63
|
-
mode = 'full',
|
|
64
84
|
} = {}) {
|
|
65
85
|
const ids = [...pageIds].sort()
|
|
66
|
-
const maxId = Math.max(
|
|
67
|
-
const tty =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
const maxId = Math.max(6, ...ids.map((id) => String(id).length))
|
|
87
|
+
const tty = useTty(stream)
|
|
88
|
+
const termW = Math.max(60, stream.columns || 100)
|
|
89
|
+
const LEFT_W = Math.min(56, Math.max(38, Math.floor(termW * 0.42)))
|
|
90
|
+
const GAP = 2
|
|
91
|
+
const RIGHT_W = Math.max(16, termW - LEFT_W - GAP)
|
|
72
92
|
|
|
73
93
|
/** @type {Map<string, { bundle: object, prerender: object }>} */
|
|
74
94
|
const rows = new Map()
|
|
@@ -79,9 +99,13 @@ export function createParallelPagesReporter ({
|
|
|
79
99
|
})
|
|
80
100
|
}
|
|
81
101
|
|
|
102
|
+
const logBuffer = []
|
|
103
|
+
const MAX_LOG = 120
|
|
82
104
|
let frame = 0
|
|
83
105
|
let spinTimer = null
|
|
84
106
|
let blockLines = 0
|
|
107
|
+
let captureActive = false
|
|
108
|
+
let savedConsole = null
|
|
85
109
|
|
|
86
110
|
const anyRunning = () => {
|
|
87
111
|
for (const r of rows.values()) {
|
|
@@ -90,44 +114,107 @@ export function createParallelPagesReporter ({
|
|
|
90
114
|
return false
|
|
91
115
|
}
|
|
92
116
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
117
|
+
function pushLog (line) {
|
|
118
|
+
const s = String(line).trim() ? String(line) : ''
|
|
119
|
+
if (!s) return
|
|
120
|
+
if (!tty) {
|
|
121
|
+
stream.write(`${s}\n`)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
logBuffer.push(dim(s))
|
|
125
|
+
if (logBuffer.length > MAX_LOG) logBuffer.splice(0, logBuffer.length - MAX_LOG)
|
|
126
|
+
redraw()
|
|
99
127
|
}
|
|
100
128
|
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
function leftHeaderLines () {
|
|
130
|
+
const out = []
|
|
131
|
+
out.push(bold(overview.title || `${scope} build`))
|
|
132
|
+
if (overview.configRel) {
|
|
133
|
+
out.push(`${dim('config')} ${truncateVisible(overview.configRel, LEFT_W - 10)}`)
|
|
105
134
|
}
|
|
135
|
+
const api = overview.apiRoutes || []
|
|
136
|
+
if (api.length === 0) {
|
|
137
|
+
/* noop */
|
|
138
|
+
} else if (api.length <= 2) {
|
|
139
|
+
for (const r of api) {
|
|
140
|
+
const line = `${dim('api')} ${truncateVisible(`${r.id} ${r.path}`, LEFT_W - 6)}`
|
|
141
|
+
out.push(line)
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
out.push(`${dim('api')} ${api.length} routes`)
|
|
145
|
+
}
|
|
146
|
+
out.push(dim('─'.repeat(Math.min(LEFT_W - 2, 44))))
|
|
147
|
+
return out
|
|
106
148
|
}
|
|
107
149
|
|
|
108
|
-
function
|
|
150
|
+
function lineForPageRow (id) {
|
|
109
151
|
const r = rows.get(id)
|
|
110
152
|
const lead = rowLeadIcon(r, mode)
|
|
153
|
+
const pathStr = idToPath[id] != null ? dim(String(idToPath[id])) : ''
|
|
111
154
|
const idCol = padVisible(dim(String(id)), maxId)
|
|
155
|
+
const pathPad = padVisible(pathStr, 14)
|
|
112
156
|
const p = fmtPart(r.prerender, 'html', frame)
|
|
113
157
|
if (mode === 'prerender-only') {
|
|
114
|
-
return ` ${lead} ${dim(scope)} ${idCol} ${p}`
|
|
158
|
+
return ` ${lead} ${dim(scope)} ${idCol} ${pathPad} ${p}`
|
|
115
159
|
}
|
|
116
160
|
const b = fmtPart(r.bundle, 'hydrate', frame)
|
|
117
|
-
return ` ${lead} ${dim(scope)} ${idCol} ${padVisible(b,
|
|
161
|
+
return ` ${lead} ${dim(scope)} ${idCol} ${pathPad} ${padVisible(b, 20)} ${p}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildLeftColumnLines () {
|
|
165
|
+
const head = leftHeaderLines()
|
|
166
|
+
const body = ids.map((id) => lineForPageRow(id))
|
|
167
|
+
return [...head, ...body]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildHeaderOnlyLines () {
|
|
171
|
+
return leftHeaderLines()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Right column row i: logs bottom-aligned to the block height */
|
|
175
|
+
function rightColumnLine (i, totalRows) {
|
|
176
|
+
const start = logBuffer.length - totalRows + i
|
|
177
|
+
if (start < 0 || start >= logBuffer.length) return dim('·')
|
|
178
|
+
return truncateVisible(logBuffer[start], RIGHT_W)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function paintRow (leftLine, rightLine) {
|
|
182
|
+
const L = padVisible(truncateVisible(leftLine, LEFT_W), LEFT_W)
|
|
183
|
+
const R = truncateVisible(rightLine, RIGHT_W)
|
|
184
|
+
return `${L}${' '.repeat(GAP)}${R}\n`
|
|
118
185
|
}
|
|
119
186
|
|
|
120
187
|
function redraw () {
|
|
121
188
|
if (!tty) return
|
|
122
|
-
const
|
|
189
|
+
const leftCol = buildLeftColumnLines()
|
|
190
|
+
const n = leftCol.length
|
|
191
|
+
const lines = []
|
|
192
|
+
for (let i = 0; i < n; i++) {
|
|
193
|
+
lines.push(paintRow(leftCol[i], rightColumnLine(i, n)))
|
|
194
|
+
}
|
|
123
195
|
if (blockLines === 0) {
|
|
124
|
-
for (const ln of lines) stream.write(
|
|
196
|
+
for (const ln of lines) stream.write(ln)
|
|
125
197
|
blockLines = lines.length
|
|
126
198
|
return
|
|
127
199
|
}
|
|
128
200
|
stream.write(`\x1b[${blockLines}A`)
|
|
129
201
|
for (const ln of lines) {
|
|
130
|
-
stream.write(`\x1b[2K\r${ln}
|
|
202
|
+
stream.write(`\x1b[2K\r${ln}`)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function ensureSpin () {
|
|
207
|
+
if (!tty || spinTimer) return
|
|
208
|
+
spinTimer = setInterval(() => {
|
|
209
|
+
frame += 1
|
|
210
|
+
redraw()
|
|
211
|
+
}, 80)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function stopSpin () {
|
|
215
|
+
if (spinTimer) {
|
|
216
|
+
clearInterval(spinTimer)
|
|
217
|
+
spinTimer = null
|
|
131
218
|
}
|
|
132
219
|
}
|
|
133
220
|
|
|
@@ -139,7 +226,58 @@ export function createParallelPagesReporter ({
|
|
|
139
226
|
if (err && !ok) stream.write(` ${red(String(err.message || err))}\n`)
|
|
140
227
|
}
|
|
141
228
|
|
|
229
|
+
function beginCapture () {
|
|
230
|
+
if (!tty || captureActive) return
|
|
231
|
+
captureActive = true
|
|
232
|
+
savedConsole = {
|
|
233
|
+
log: console.log,
|
|
234
|
+
warn: console.warn,
|
|
235
|
+
info: console.info,
|
|
236
|
+
error: console.error,
|
|
237
|
+
}
|
|
238
|
+
const fmt = (...a) => util.format(...a)
|
|
239
|
+
console.log = (...a) => {
|
|
240
|
+
pushLog(`[log] ${fmt(...a)}`)
|
|
241
|
+
}
|
|
242
|
+
console.warn = (...a) => {
|
|
243
|
+
pushLog(`[warn] ${fmt(...a)}`)
|
|
244
|
+
}
|
|
245
|
+
console.info = (...a) => {
|
|
246
|
+
pushLog(`[info] ${fmt(...a)}`)
|
|
247
|
+
}
|
|
248
|
+
console.error = (...a) => {
|
|
249
|
+
const msg = fmt(...a)
|
|
250
|
+
pushLog(`[error] ${msg}`)
|
|
251
|
+
savedConsole.error(`[@ossy/app] ${msg}`)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function endCapture () {
|
|
256
|
+
if (!captureActive || !savedConsole) return
|
|
257
|
+
console.log = savedConsole.log
|
|
258
|
+
console.warn = savedConsole.warn
|
|
259
|
+
console.info = savedConsole.info
|
|
260
|
+
console.error = savedConsole.error
|
|
261
|
+
savedConsole = null
|
|
262
|
+
captureActive = false
|
|
263
|
+
}
|
|
264
|
+
|
|
142
265
|
return {
|
|
266
|
+
pushLog,
|
|
267
|
+
|
|
268
|
+
/** Start optional console hijack + first paint (TTY). Plain header only when not a TTY. */
|
|
269
|
+
start () {
|
|
270
|
+
if (!tty) {
|
|
271
|
+
for (const ln of buildHeaderOnlyLines()) {
|
|
272
|
+
stream.write(`${ln}\n`)
|
|
273
|
+
}
|
|
274
|
+
stream.write('\n')
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
beginCapture()
|
|
278
|
+
redraw()
|
|
279
|
+
},
|
|
280
|
+
|
|
143
281
|
startBundle (pageId) {
|
|
144
282
|
if (mode === 'prerender-only') return
|
|
145
283
|
const r = rows.get(pageId)
|
|
@@ -188,23 +326,18 @@ export function createParallelPagesReporter ({
|
|
|
188
326
|
redraw()
|
|
189
327
|
},
|
|
190
328
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
stopSpin()
|
|
194
|
-
if (tty && blockLines > 0) {
|
|
195
|
-
redraw()
|
|
196
|
-
stream.write('\n')
|
|
197
|
-
blockLines = 0
|
|
198
|
-
}
|
|
329
|
+
printSectionTitle () {
|
|
330
|
+
/* merged into overview.title — no-op for API compat */
|
|
199
331
|
},
|
|
200
332
|
|
|
201
|
-
|
|
333
|
+
dispose () {
|
|
202
334
|
stopSpin()
|
|
335
|
+
endCapture()
|
|
203
336
|
if (tty && blockLines > 0) {
|
|
337
|
+
redraw()
|
|
204
338
|
stream.write('\n')
|
|
205
339
|
blockLines = 0
|
|
206
340
|
}
|
|
207
|
-
stream.write(`\n ${bold(scope)} ${dim(title)}\n`)
|
|
208
341
|
},
|
|
209
342
|
}
|
|
210
343
|
}
|