@ossy/app 1.11.7 → 1.11.9

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.
@@ -1,29 +1,8 @@
1
1
  import path from 'path'
2
- import url from 'url'
3
2
  import fs from 'fs'
4
3
  import { rollup } from 'rollup'
5
- import { BuildPage, buildPrerenderAppConfig } from './render-page.task.js'
6
4
  import { pageIdFromHydrateEntryName } from './build-terminal.js'
7
5
 
8
- /**
9
- * Maps an app route path to the file path express.static will serve for that URL
10
- * (`/` → `public/index.html`, `/a/b` → `public/a/b/index.html`).
11
- */
12
- export function staticHtmlPathForRoute (routePath, publicDir) {
13
- const p = typeof routePath === 'string' ? routePath : '/'
14
- if (p === '/' || p === '') {
15
- return path.join(publicDir, 'index.html')
16
- }
17
- const segments = p.replace(/^\//, '').split('/').filter(Boolean)
18
- return path.join(publicDir, ...segments, 'index.html')
19
- }
20
-
21
- function pathIsPrerenderable (routePath) {
22
- if (typeof routePath !== 'string') return false
23
- if (!routePath.startsWith('/') || routePath.includes(':')) return false
24
- return true
25
- }
26
-
27
6
  function copyPublicToBuild ({ copyPublicFrom, buildPath }) {
28
7
  if (!copyPublicFrom || !fs.existsSync(copyPublicFrom)) return
29
8
  const dest = path.join(buildPath, 'public')
@@ -31,10 +10,6 @@ function copyPublicToBuild ({ copyPublicFrom, buildPath }) {
31
10
  fs.cpSync(copyPublicFrom, dest, { recursive: true })
32
11
  }
33
12
 
34
- function countSettledFailures (results) {
35
- return results.filter((r) => r.status === 'rejected').length
36
- }
37
-
38
13
  async function bundleOneHydratePage ({
39
14
  entryName,
40
15
  stubPath,
@@ -113,156 +88,37 @@ async function bundleHydratePagesParallel ({
113
88
  })
114
89
  )
115
90
 
116
- const failures = countSettledFailures(results)
117
-
118
- return { results, failures }
119
- }
120
-
121
- async function prerenderOnePage ({
122
- route,
123
- buildTimeConfig,
124
- pageList,
125
- publicDir,
126
- }) {
127
- const appConfig = buildPrerenderAppConfig({
128
- buildTimeConfig,
129
- pageList,
130
- activeRouteId: route.id,
131
- urlPath: route.path,
132
- })
133
- const html = await BuildPage.handle({
134
- route,
135
- appConfig,
136
- isDevReloadEnabled: false,
137
- })
138
- const outPath = staticHtmlPathForRoute(route.path, publicDir)
139
- fs.mkdirSync(path.dirname(outPath), { recursive: true })
140
- fs.writeFileSync(outPath, html, 'utf8')
141
- }
142
-
143
- async function prerenderPagesParallel ({
144
- pagesEntryPath,
145
- configSourcePath,
146
- publicDir,
147
- reporter,
148
- }) {
149
- const cfgHref = url.pathToFileURL(path.resolve(configSourcePath)).href
150
- const pagesHref = url.pathToFileURL(path.resolve(pagesEntryPath)).href
151
-
152
- const configModule = await import(cfgHref)
153
- const pagesModule = await import(pagesHref)
154
-
155
- const buildTimeConfig = configModule?.default ?? configModule ?? {}
156
- const pageList = pagesModule?.default ?? []
157
-
158
- const routesToRender = []
159
- for (const route of pageList) {
160
- if (typeof route?.module !== 'string' || !route.module) continue
161
- if (!pathIsPrerenderable(route.path)) {
162
- reporter?.skipPrerender?.(
163
- route.id,
164
- `not static (${JSON.stringify(route.path)})`
165
- )
166
- console.warn(
167
- `[@ossy/app][prerender] Skipping "${route.id}" (path not prerenderable: ${JSON.stringify(route.path)})`
168
- )
169
- continue
170
- }
171
- routesToRender.push(route)
172
- }
173
-
174
- const results = await Promise.allSettled(
175
- routesToRender.map(async (route) => {
176
- const t0 = Date.now()
177
- reporter?.startPrerender?.(route.id)
178
- try {
179
- await prerenderOnePage({
180
- route,
181
- buildTimeConfig,
182
- pageList,
183
- publicDir,
184
- })
185
- reporter?.completePrerender?.(route.id, { ok: true, ms: Date.now() - t0 })
186
- } catch (error) {
187
- reporter?.completePrerender?.(route.id, {
188
- ok: false,
189
- ms: Date.now() - t0,
190
- error,
191
- })
192
- console.error(
193
- `[@ossy/app][prerender] "${route?.id}" (${route?.path}) failed:`,
194
- error
195
- )
196
- throw error
197
- }
198
- })
199
- )
200
- const failures = countSettledFailures(results)
201
-
91
+ const failures = results.filter((r) => r.status === 'rejected').length
202
92
  return { results, failures }
203
93
  }
204
94
 
205
- async function runProduction ({
206
- clientHydrateInput,
207
- pageFilesLength,
208
- copyPublicFrom,
209
- buildPath,
210
- nodeEnv,
211
- pagesEntryPath,
212
- configSourcePath,
213
- createClientRollupPlugins,
214
- minifyBrowserStaticChunks,
215
- reporter,
216
- }) {
217
- if (pageFilesLength === 0 || Object.keys(clientHydrateInput).length === 0) {
218
- return { bundleFailures: 0, prerenderFailures: 0 }
219
- }
220
-
221
- const { failures: bundleFailures } = await bundleHydratePagesParallel({
222
- clientHydrateInput,
223
- buildPath,
224
- copyPublicFrom,
225
- nodeEnv,
226
- buildPathForPlugins: buildPath,
227
- createClientRollupPlugins,
228
- minifyBrowserStaticChunks,
229
- reporter,
230
- })
231
-
232
- const { failures: prerenderFailures } = await prerenderPagesParallel({
233
- pagesEntryPath,
234
- configSourcePath,
235
- publicDir: path.join(buildPath, 'public'),
236
- reporter,
237
- })
238
-
239
- if (bundleFailures + prerenderFailures > 0) {
240
- console.warn(
241
- `[@ossy/app][build] Finished with ${bundleFailures} client bundle error(s) and ${prerenderFailures} prerender error(s)`
242
- )
243
- }
244
-
245
- return { bundleFailures, prerenderFailures }
246
- }
247
-
248
95
  export default {
249
96
  type: '@ossy/app/prerender-react',
250
- /** `input.op`: `runProduction` | `prerenderPagesParallel` (see callers in `build.js` / `dev.js`). */
251
97
  async handler (input) {
252
98
  const op = input?.op
253
99
  if (op === 'runProduction') {
254
- return runProduction(input)
255
- }
256
- if (op === 'prerenderPagesParallel') {
257
- return prerenderPagesParallel({
258
- pagesEntryPath: input.pagesEntryPath,
259
- configSourcePath: input.configSourcePath,
260
- publicDir: input.publicDir,
261
- reporter: input.reporter,
100
+ const { clientHydrateInput, pageFilesLength, copyPublicFrom, buildPath, nodeEnv,
101
+ buildPathForPlugins, createClientRollupPlugins, minifyBrowserStaticChunks, reporter } = input
102
+ if (pageFilesLength === 0 || Object.keys(clientHydrateInput).length === 0) {
103
+ return { bundleFailures: 0 }
104
+ }
105
+ const { failures: bundleFailures } = await bundleHydratePagesParallel({
106
+ clientHydrateInput,
107
+ buildPath,
108
+ copyPublicFrom,
109
+ nodeEnv,
110
+ buildPathForPlugins: buildPathForPlugins ?? buildPath,
111
+ createClientRollupPlugins,
112
+ minifyBrowserStaticChunks,
113
+ reporter,
262
114
  })
115
+ if (bundleFailures > 0) {
116
+ console.warn(`[@ossy/app][build] Finished with ${bundleFailures} client bundle error(s)`)
117
+ }
118
+ return { bundleFailures }
263
119
  }
264
120
  throw new Error(
265
- `[@ossy/app][prerender-react] Unknown op: ${String(op)} (expected runProduction | prerenderPagesParallel)`
121
+ `[@ossy/app][prerender-react] Unknown op: ${String(op)} (expected runProduction)`
266
122
  )
267
123
  },
268
124
  }
@@ -1,30 +1,22 @@
1
1
  import path from 'node:path'
2
2
  import { fileURLToPath, pathToFileURL } from 'node:url'
3
- import React, { createElement } from 'react'
4
- import { prerenderToNodeStream } from 'react-dom/static'
5
3
 
6
4
  const __ossyDir = path.dirname(fileURLToPath(import.meta.url))
7
5
 
8
- async function loadPageDefaultExport (route) {
9
- if (typeof route?.module !== 'string' || !route.module) {
10
- throw new Error(
11
- `[@ossy/app][BuildPage] Route "${route?.id ?? '?'}" has no compiled module path (re-run build so pages.generated.json includes "module").`
12
- )
6
+ async function loadSsrBundle (route) {
7
+ if (typeof route?.id !== 'string' || !route.id) {
8
+ throw new Error(`[@ossy/app][BuildPage] Route has no id`)
13
9
  }
14
- const abs = path.join(__ossyDir, route.module)
15
- const mod = await import(pathToFileURL(abs).href)
16
- const def = mod?.default
17
- if (typeof def !== 'function') {
10
+ const bundlePath = path.join(__ossyDir, '..', 'ssr', `${route.id}.mjs`)
11
+ const mod = await import(pathToFileURL(bundlePath).href)
12
+ if (typeof mod?.renderPage !== 'function') {
18
13
  throw new Error(
19
- `[@ossy/app][BuildPage] Page "${route?.id}" must export default as a function component (got ${typeof def}).`
14
+ `[@ossy/app][BuildPage] SSR bundle for "${route.id}" must export renderPage (got ${typeof mod?.renderPage}).`
20
15
  )
21
16
  }
22
- return def
17
+ return mod
23
18
  }
24
19
 
25
- /**
26
- * App shell config for SSR / prerender (mirrors client: theme, pages metadata, props for the active page).
27
- */
28
20
  export function buildPrerenderAppConfig ({
29
21
  buildTimeConfig,
30
22
  pageList,
@@ -32,11 +24,9 @@ export function buildPrerenderAppConfig ({
32
24
  urlPath,
33
25
  isAuthenticated = false,
34
26
  }) {
35
- /** `module` is the compiled page path under `.ossy/` (Node `import()` only; not loaded as raw ESM in the browser). */
36
27
  const pages = pageList.map((page) => ({
37
28
  id: page?.id,
38
29
  path: page?.path,
39
- ...(typeof page?.module === 'string' ? { module: page.module } : {}),
40
30
  }))
41
31
  return {
42
32
  ...buildTimeConfig,
@@ -50,23 +40,8 @@ export function buildPrerenderAppConfig ({
50
40
  }
51
41
  }
52
42
 
53
- /** Strips non-JSON content for the bootstrap script; keeps serializable route fields including `module`. */
54
- export function appConfigForBootstrap (appConfig) {
55
- if (!appConfig || typeof appConfig !== 'object') return appConfig
56
- const pages = Array.isArray(appConfig.pages)
57
- ? appConfig.pages.map(({ id, path, module }) => ({
58
- id,
59
- path,
60
- ...(typeof module === 'string' ? { module } : {}),
61
- }))
62
- : appConfig.pages
63
- return { ...appConfig, pages }
64
- }
65
-
66
- /** Plain data clone so server render props match `JSON.parse(JSON.stringify(...))` on the client. */
67
43
  function jsonSafeClone (value) {
68
44
  if (value == null || typeof value !== 'object') return value
69
- if (typeof value === 'function' || React.isValidElement(value)) return value
70
45
  try {
71
46
  return JSON.parse(JSON.stringify(value))
72
47
  } catch {
@@ -74,42 +49,24 @@ function jsonSafeClone (value) {
74
49
  }
75
50
  }
76
51
 
77
- /**
78
- * Props passed to `<App>` for SSR and the exact object embedded in `window.__INITIAL_APP_CONFIG__`.
79
- * Keeps server and hydrate trees aligned (fixes React #418 hydration mismatches).
80
- */
81
52
  export function buildHydrationAppConfig (appConfig) {
82
- const base = appConfigForBootstrap(appConfig)
53
+ if (!appConfig || typeof appConfig !== 'object') return appConfig
83
54
  return {
84
- ...base,
85
- theme: jsonSafeClone(base.theme),
86
- themes: jsonSafeClone(base.themes),
87
- resourceTemplates: jsonSafeClone(base.resourceTemplates),
55
+ ...appConfig,
56
+ theme: jsonSafeClone(appConfig.theme),
57
+ themes: jsonSafeClone(appConfig.themes),
58
+ resourceTemplates: jsonSafeClone(appConfig.resourceTemplates),
88
59
  }
89
60
  }
90
61
 
91
62
  export const BuildPage = {
92
- async handle ({ route, appConfig, isDevReloadEnabled }) {
63
+ async handle ({ route, appConfig }) {
93
64
  const hydrationConfig = buildHydrationAppConfig(appConfig)
94
- const Page = await loadPageDefaultExport(route)
95
- const rootElement = createElement(Page, hydrationConfig)
96
- const devReloadScript = isDevReloadEnabled
97
- ? `(function(){try{var es=new EventSource('/__ossy_reload');es.addEventListener('reload',function(){location.reload();});}catch(e){}})();`
98
- : ``
99
-
100
- const hydrateUrl = `/static/${route.id}.js`
101
- const { prelude } = await prerenderToNodeStream(rootElement, {
102
- bootstrapScriptContent: `window.__INITIAL_APP_CONFIG__ = ${JSON.stringify(hydrationConfig)};${devReloadScript}`,
103
- bootstrapModules: [hydrateUrl],
104
- })
65
+ const { renderPage } = await loadSsrBundle(route)
105
66
 
106
- return new Promise((resolve, reject) => {
107
- let data = ''
108
- prelude.on('data', (chunk) => {
109
- data += chunk
110
- })
111
- prelude.on('end', () => resolve(data))
112
- prelude.on('error', reject)
67
+ return renderPage(hydrationConfig, {
68
+ bootstrapScriptContent: `window.__INITIAL_APP_CONFIG__=${JSON.stringify(hydrationConfig)}`,
69
+ bootstrapModules: [`/static/${route.id}.js`],
113
70
  })
114
71
  },
115
72
  }
package/cli/server.js CHANGED
@@ -39,9 +39,6 @@ const app = express();
39
39
  const currentDir = path.dirname(url.fileURLToPath(import.meta.url))
40
40
  const ROOT_PATH = path.resolve(currentDir, 'public')
41
41
 
42
- const isDevReloadEnabled = process.env.OSSY_DEV_RELOAD === '1'
43
- const reloadClients = new Set()
44
-
45
42
  function parsePortFromArgv(argv) {
46
43
  // Supports: --port 4000, --port=4000, -p 4000
47
44
  const idx = argv.findIndex(a => a === '--port' || a === '-p')
@@ -67,33 +64,6 @@ if (Middleware !== undefined) {
67
64
  console.log(`[@ossy/app][server] ${Middleware?.length || 0} custom middleware loaded`)
68
65
  }
69
66
 
70
- if (isDevReloadEnabled) {
71
- app.get('/__ossy_reload', (req, res) => {
72
- res.status(200)
73
- res.setHeader('Content-Type', 'text/event-stream')
74
- res.setHeader('Cache-Control', 'no-cache')
75
- res.setHeader('Connection', 'keep-alive')
76
- res.flushHeaders?.()
77
-
78
- res.write('event: connected\ndata: ok\n\n')
79
- reloadClients.add(res)
80
-
81
- req.on('close', () => {
82
- reloadClients.delete(res)
83
- })
84
- })
85
-
86
- app.post('/__ossy_reload', (req, res) => {
87
- for (const client of reloadClients) {
88
- try {
89
- client.write('event: reload\ndata: now\n\n')
90
- } catch {
91
- // ignore broken connections
92
- }
93
- }
94
- res.status(204).end()
95
- })
96
- }
97
67
 
98
68
  const middleware = [
99
69
  morgan('tiny'),
@@ -163,11 +133,7 @@ app.all('*all', async (req, res) => {
163
133
  urlPath: requestUrl,
164
134
  isAuthenticated: !!req.isAuthenticated,
165
135
  })
166
- const html = await BuildPage.handle({
167
- route: pageRoute,
168
- appConfig,
169
- isDevReloadEnabled,
170
- })
136
+ const html = await BuildPage.handle({ route: pageRoute, appConfig })
171
137
  res.status(200).type('html').send(html)
172
138
  return
173
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "1.11.7",
3
+ "version": "1.11.9",
4
4
  "description": "",
5
5
  "source": "./src/index.js",
6
6
  "main": "./src/index.js",
@@ -27,14 +27,14 @@
27
27
  "@babel/eslint-parser": "^7.15.8",
28
28
  "@babel/preset-react": "^7.26.3",
29
29
  "@babel/register": "^7.25.9",
30
- "@ossy/connected-components": "^1.11.7",
31
- "@ossy/design-system": "^1.11.7",
32
- "@ossy/pages": "^1.11.7",
33
- "@ossy/router": "^1.11.7",
34
- "@ossy/router-react": "^1.11.7",
35
- "@ossy/sdk": "^1.11.7",
36
- "@ossy/sdk-react": "^1.11.7",
37
- "@ossy/themes": "^1.11.7",
30
+ "@ossy/connected-components": "^1.11.9",
31
+ "@ossy/design-system": "^1.11.9",
32
+ "@ossy/pages": "^1.11.9",
33
+ "@ossy/router": "^1.11.9",
34
+ "@ossy/router-react": "^1.11.9",
35
+ "@ossy/sdk": "^1.11.9",
36
+ "@ossy/sdk-react": "^1.11.9",
37
+ "@ossy/themes": "^1.11.9",
38
38
  "@rollup/plugin-alias": "^6.0.0",
39
39
  "@rollup/plugin-babel": "6.1.0",
40
40
  "@rollup/plugin-commonjs": "^29.0.0",
@@ -67,5 +67,5 @@
67
67
  "README.md",
68
68
  "tsconfig.json"
69
69
  ],
70
- "gitHead": "43d1eca8557efc9586398a893ce9832b2ebf0444"
70
+ "gitHead": "6644afdbe9d5eb8ed1f79657d14b1268ad283bcb"
71
71
  }