@ossy/app 1.15.0 → 1.15.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/cli/build.task.js +74 -0
- package/cli/get-files.task copy.js +234 -0
- package/cli/getPlatformFiles.task.js +51 -0
- package/cli/index.js +7 -2
- package/cli/start.task.js +19 -0
- package/package.json +11 -11
- package/src/index.js +1 -1
- package/cli/Middleware.js +0 -3
- package/cli/build-terminal.js +0 -242
- package/cli/build.js +0 -972
- package/cli/default-config.js +0 -5
- package/cli/prerender-react.task.js +0 -8
- package/cli/render-page.task.js +0 -51
- package/scripts/ensure-build-stubs.mjs +0 -35
package/cli/build.js
DELETED
|
@@ -1,972 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import url from 'url';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import { pathToFileURL } from 'node:url'
|
|
5
|
-
import { createRequire } from 'node:module'
|
|
6
|
-
import { rollup } from 'rollup';
|
|
7
|
-
import babel from '@rollup/plugin-babel';
|
|
8
|
-
import { nodeResolve as resolveDependencies } from '@rollup/plugin-node-resolve'
|
|
9
|
-
import resolveCommonJsDependencies from '@rollup/plugin-commonjs'
|
|
10
|
-
import { minify as minifyWithTerser } from 'terser'
|
|
11
|
-
import json from "@rollup/plugin-json"
|
|
12
|
-
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'
|
|
16
|
-
|
|
17
|
-
export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
|
|
18
|
-
export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
|
|
19
|
-
export const TASK_FILE_PATTERN = /\.task\.(mjs|cjs|js)$/
|
|
20
|
-
const RESOURCE_TEMPLATE_FILE_PATTERN = /\.resource\.js$/
|
|
21
|
-
|
|
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
|
-
export const OSSY_GEN_DIRNAME = '.ossy'
|
|
53
|
-
/** JSON-only manifests (sources + route metadata). */
|
|
54
|
-
export const OSSY_GEN_PAGES_BASENAME = 'pages.generated.json'
|
|
55
|
-
export const OSSY_GEN_API_BASENAME = 'api.generated.json'
|
|
56
|
-
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/` */
|
|
90
|
-
const HYDRATE_ENTRY_FILENAME = 'hydrate-entry.jsx'
|
|
91
|
-
/** Shared SSR entry filename under `.ossy/` */
|
|
92
|
-
const SSR_ENTRY_FILENAME = 'ssr-entry.mjs'
|
|
93
|
-
|
|
94
|
-
export function ossyGeneratedDir (buildPath) {
|
|
95
|
-
return path.join(buildPath, OSSY_GEN_DIRNAME)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function ensureOssyGeneratedDir (buildPath) {
|
|
99
|
-
const dir = ossyGeneratedDir(buildPath)
|
|
100
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
101
|
-
return dir
|
|
102
|
-
}
|
|
103
|
-
|
|
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) {
|
|
110
|
-
fs.rmSync(buildPath, { recursive: true, force: true })
|
|
111
|
-
ensureOssyGeneratedDir(buildPath)
|
|
112
|
-
}
|
|
113
|
-
|
|
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
|
-
)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Recursively lists files under `srcDir` whose basename matches `filePattern` (e.g. `/\.api\.js$/`).
|
|
295
|
-
*/
|
|
296
|
-
export function discoverFilesByPattern (srcDir, filePattern) {
|
|
297
|
-
const dir = path.resolve(srcDir)
|
|
298
|
-
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
299
|
-
return []
|
|
300
|
-
}
|
|
301
|
-
const files = []
|
|
302
|
-
const walk = (d) => {
|
|
303
|
-
const entries = fs.readdirSync(d, { withFileTypes: true })
|
|
304
|
-
for (const e of entries) {
|
|
305
|
-
const full = path.join(d, e.name)
|
|
306
|
-
if (e.isDirectory()) walk(full)
|
|
307
|
-
else if (filePattern.test(e.name)) files.push(full)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
walk(dir)
|
|
311
|
-
return files.sort()
|
|
312
|
-
}
|
|
313
|
-
|
|
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
|
-
export function buildTasksManifestPayload (taskFiles, cwd = process.cwd()) {
|
|
326
|
-
return taskFiles.map((f) => path.relative(cwd, f).replace(/\\/g, '/'))
|
|
327
|
-
}
|
|
328
|
-
|
|
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
|
|
415
|
-
}
|
|
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) {
|
|
425
|
-
return []
|
|
426
|
-
}
|
|
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
|
-
}
|
|
449
|
-
|
|
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)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export function filePathToRoute(filePath, srcDir) {
|
|
470
|
-
const rel = path.relative(srcDir, filePath).replace(/\\/g, '/')
|
|
471
|
-
let pathPart = rel.replace(PAGE_FILE_PATTERN, '').replace(/\/index$/, '').replace(/\/home$/, '') || 'home'
|
|
472
|
-
if (pathPart === 'index' || pathPart === 'home') pathPart = 'home'
|
|
473
|
-
const id = pathPart === 'home' ? 'home' : pathPart.replace(/\//g, '-')
|
|
474
|
-
const routePath = pathPart === 'home' ? '/' : '/' + pathPart
|
|
475
|
-
return { id, path: routePath }
|
|
476
|
-
}
|
|
477
|
-
|
|
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) {
|
|
502
|
-
const derived = filePathToRoute(pageAbsPath, srcDir)
|
|
503
|
-
try {
|
|
504
|
-
const src = fs.readFileSync(pageAbsPath, 'utf8')
|
|
505
|
-
const metaIdx = src.indexOf('export const metadata')
|
|
506
|
-
if (metaIdx === -1) return derived
|
|
507
|
-
const after = src.slice(metaIdx)
|
|
508
|
-
const id = after.match(/\bid\s*:\s*['"]([^'"]+)['"]/)?.[1] ?? derived.id
|
|
509
|
-
const strPath = after.match(/\bpath\s*:\s*['"]([^'"]+)['"]/)?.[1]
|
|
510
|
-
if (strPath) {
|
|
511
|
-
return { id, path: strPath }
|
|
512
|
-
}
|
|
513
|
-
const pathObjBody = after.match(/\bpath\s*:\s*\{([\s\S]*?)\}/)?.[1]
|
|
514
|
-
if (pathObjBody) {
|
|
515
|
-
const languagePathEntries = [...pathObjBody.matchAll(/([A-Za-z0-9_]+)\s*:\s*['"]([^'"]+)['"]/g)]
|
|
516
|
-
if (languagePathEntries.length > 0) {
|
|
517
|
-
return {
|
|
518
|
-
id,
|
|
519
|
-
path: Object.fromEntries(languagePathEntries.map(([, language, routePath]) => [language, routePath])),
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
return { id, path: derived.path }
|
|
524
|
-
} catch {
|
|
525
|
-
return derived
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
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
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
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
|
-
)
|
|
565
|
-
}
|
|
566
|
-
|
|
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 }) {
|
|
573
|
-
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
|
-
)
|
|
581
|
-
}
|
|
582
|
-
seenIds.add(hydrateId)
|
|
583
|
-
const rel = relToGeneratedImport(stubAbsPath, f)
|
|
584
|
-
pageLines.push(` ${JSON.stringify(hydrateId)}: () => import('./${rel}'),`)
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
return [
|
|
588
|
-
'// Generated by @ossy/app — do not edit',
|
|
589
|
-
'',
|
|
590
|
-
"import { createElement } from 'react'",
|
|
591
|
-
"import { hydrateRoot } from 'react-dom/client'",
|
|
592
|
-
"import { App } from '@ossy/connected-components'",
|
|
593
|
-
'',
|
|
594
|
-
'const config = window.__INITIAL_APP_CONFIG__ || {}',
|
|
595
|
-
'',
|
|
596
|
-
'const pages = {',
|
|
597
|
-
...pageLines,
|
|
598
|
-
'}',
|
|
599
|
-
'',
|
|
600
|
-
'const load = pages[config.pageId]',
|
|
601
|
-
'if (load) {',
|
|
602
|
-
' load().then((mod) => {',
|
|
603
|
-
' const Page = mod.default',
|
|
604
|
-
" const metadata = mod.metadata || {}",
|
|
605
|
-
' function PageShell (props) {',
|
|
606
|
-
" return createElement('html', { lang: props.defaultLanguage || 'en' },",
|
|
607
|
-
" createElement('head', null,",
|
|
608
|
-
" createElement('meta', { charSet: 'utf-8' }),",
|
|
609
|
-
" createElement('title', null, metadata.title || ''),",
|
|
610
|
-
' ),',
|
|
611
|
-
' createElement(App, props,',
|
|
612
|
-
' createElement(Page, props)',
|
|
613
|
-
' )',
|
|
614
|
-
' )',
|
|
615
|
-
' }',
|
|
616
|
-
' hydrateRoot(document, createElement(PageShell, config))',
|
|
617
|
-
' })',
|
|
618
|
-
'}',
|
|
619
|
-
'',
|
|
620
|
-
].join('\n')
|
|
621
|
-
}
|
|
622
|
-
|
|
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 }) {
|
|
646
|
-
const seenIds = new Set()
|
|
647
|
-
const pagesLiteral = []
|
|
648
|
-
const pageModuleLines = []
|
|
649
|
-
|
|
650
|
-
for (const f of pageFiles) {
|
|
651
|
-
const { id, path: routePath } = pageRouteFromSource(f, srcDir)
|
|
652
|
-
if (seenIds.has(id)) {
|
|
653
|
-
throw new Error(
|
|
654
|
-
`[@ossy/app] Duplicate page id "${id}" (${f}). Pages need unique ids.`
|
|
655
|
-
)
|
|
656
|
-
}
|
|
657
|
-
seenIds.add(id)
|
|
658
|
-
const rel = relToGeneratedImport(stubAbsPath, f)
|
|
659
|
-
pagesLiteral.push(` { id: ${JSON.stringify(id)}, path: ${JSON.stringify(routePath)} },`)
|
|
660
|
-
pageModuleLines.push(` ${JSON.stringify(id)}: () => import('./${rel}'),`)
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
return [
|
|
664
|
-
'// Generated by @ossy/app — do not edit',
|
|
665
|
-
'',
|
|
666
|
-
"import { createElement } from 'react'",
|
|
667
|
-
"import { renderToPipeableStream } from 'react-dom/server'",
|
|
668
|
-
"import { Writable } from 'node:stream'",
|
|
669
|
-
"import { App } from '@ossy/connected-components'",
|
|
670
|
-
'',
|
|
671
|
-
'export const pages = [',
|
|
672
|
-
...pagesLiteral,
|
|
673
|
-
']',
|
|
674
|
-
'',
|
|
675
|
-
'const pageModules = {',
|
|
676
|
-
...pageModuleLines,
|
|
677
|
-
'}',
|
|
678
|
-
'',
|
|
679
|
-
'function PageShell (props) {',
|
|
680
|
-
" const meta = props._pageMeta || {}",
|
|
681
|
-
" return createElement('html', { lang: props.defaultLanguage || 'en' },",
|
|
682
|
-
" createElement('head', null,",
|
|
683
|
-
" createElement('meta', { charSet: 'utf-8' }),",
|
|
684
|
-
" createElement('title', null, meta.title || ''),",
|
|
685
|
-
' ),',
|
|
686
|
-
' createElement(App, props,',
|
|
687
|
-
' createElement(props._pageComponent, props)',
|
|
688
|
-
' )',
|
|
689
|
-
' )',
|
|
690
|
-
'}',
|
|
691
|
-
'',
|
|
692
|
-
'export async function renderPage (pageId, props, options = {}) {',
|
|
693
|
-
' const load = pageModules[pageId]',
|
|
694
|
-
" if (!load) throw new Error(`[@ossy/app] Unknown page id: ${pageId}`)",
|
|
695
|
-
' const mod = await load()',
|
|
696
|
-
' const merged = { ...props, _pageComponent: mod.default, _pageMeta: mod.metadata || {} }',
|
|
697
|
-
' return new Promise((resolve, reject) => {',
|
|
698
|
-
" let html = ''",
|
|
699
|
-
' const writable = new Writable({',
|
|
700
|
-
' write (chunk, _enc, cb) { html += chunk.toString(); cb() },',
|
|
701
|
-
' })',
|
|
702
|
-
' const { pipe } = renderToPipeableStream(createElement(PageShell, merged), {',
|
|
703
|
-
' ...options,',
|
|
704
|
-
' onAllReady () { pipe(writable) },',
|
|
705
|
-
' onError (err) { reject(err) },',
|
|
706
|
-
' })',
|
|
707
|
-
" writable.on('finish', () => resolve(html))",
|
|
708
|
-
' })',
|
|
709
|
-
'}',
|
|
710
|
-
'',
|
|
711
|
-
].join('\n')
|
|
712
|
-
}
|
|
713
|
-
|
|
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
|
|
720
|
-
}
|
|
721
|
-
|
|
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)
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const bundle = await rollup({
|
|
749
|
-
input: { server: ssrEntryPath, app: clientEntryPath },
|
|
750
|
-
plugins: createCombinedBundlePlugins({ nodeEnv }),
|
|
751
|
-
onwarn (warning, defaultHandler) {
|
|
752
|
-
if (onWarn) { onWarn(warning); return }
|
|
753
|
-
defaultHandler(warning)
|
|
754
|
-
},
|
|
755
|
-
})
|
|
756
|
-
await bundle.write({
|
|
757
|
-
dir: buildPath,
|
|
758
|
-
format: 'esm',
|
|
759
|
-
entryFileNames: (chunk) =>
|
|
760
|
-
chunk.name === 'server'
|
|
761
|
-
? 'ssr/app.mjs'
|
|
762
|
-
: 'public/static/app.js',
|
|
763
|
-
chunkFileNames: 'public/static/chunks/[name]-[hash].js',
|
|
764
|
-
plugins: [minifyBrowserStaticChunks()],
|
|
765
|
-
})
|
|
766
|
-
await bundle.close()
|
|
767
|
-
}
|
|
768
|
-
|
|
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
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
export function writePagesManifest ({
|
|
783
|
-
pageFiles,
|
|
784
|
-
srcDir,
|
|
785
|
-
pagesGeneratedPath,
|
|
786
|
-
cwd = process.cwd(),
|
|
787
|
-
}) {
|
|
788
|
-
writeOssyJson(pagesGeneratedPath, buildPagesGeneratedPayload(pageFiles, srcDir, cwd))
|
|
789
|
-
}
|
|
790
|
-
|
|
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
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
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 []
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
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)
|
|
850
|
-
|
|
851
|
-
const apiRoutes = []
|
|
852
|
-
for (const f of apiOverviewFiles) {
|
|
853
|
-
if (fs.existsSync(f)) apiRoutes.push(...parseIdPathPairsFromFile(f))
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
return { configRel, pages, apiRoutes }
|
|
857
|
-
}
|
|
858
|
-
|
|
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)')
|
|
878
|
-
}
|
|
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
|
-
})
|
|
885
|
-
}
|
|
886
|
-
console.log(' ' + '─'.repeat(50) + '\n')
|
|
887
|
-
}
|
|
888
|
-
|
|
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')
|
|
925
|
-
|
|
926
|
-
if (!fs.existsSync(middlewareSourcePath)) {
|
|
927
|
-
middlewareSourcePath = path.resolve(scriptDir, 'middleware.js')
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
const configSourcePath = fs.existsSync(configPath)
|
|
931
|
-
? configPath
|
|
932
|
-
: path.resolve(scriptDir, 'default-config.js')
|
|
933
|
-
|
|
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
|
-
})
|
|
955
|
-
|
|
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,
|
|
969
|
-
})
|
|
970
|
-
|
|
971
|
-
console.log(' \x1b[32m✔\x1b[0m \x1b[1m@ossy/app\x1b[0m \x1b[2mbuild finished\x1b[0m\n')
|
|
972
|
-
};
|