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