@ossy/app 1.9.0 → 1.10.0

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 CHANGED
@@ -11,7 +11,7 @@ import nodeExternals from 'rollup-plugin-node-externals'
11
11
  import copy from 'rollup-plugin-copy';
12
12
  import replace from '@rollup/plugin-replace';
13
13
  import arg from 'arg'
14
- import { writePrerenderedPages } from './prerender-pages.js'
14
+ import prerenderReactTask from './prerender-react.task.js'
15
15
 
16
16
  export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
17
17
  export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
@@ -24,7 +24,7 @@ export const OSSY_RESOURCE_TEMPLATES_OUT = '.ossy-system-templates.generated.js'
24
24
  /** Rollup output paths for JS served to the browser (see `entryFileNames` / `chunkFileNames`). */
25
25
  const BROWSER_STATIC_PREFIX = 'public/static/'
26
26
 
27
- function minifyBrowserStaticChunks () {
27
+ export function minifyBrowserStaticChunks () {
28
28
  return {
29
29
  name: 'minify-browser-static-chunks',
30
30
  async renderChunk (code, chunk, outputOptions) {
@@ -677,38 +677,18 @@ export const build = async (cliArgs) => {
677
677
  })
678
678
  copyOssyAppRuntime({ scriptDir, buildPath })
679
679
 
680
- const clientPlugins = createOssyClientRollupPlugins({
681
- nodeEnv: 'production',
682
- copyPublicFrom: publicDir,
683
- buildPath,
684
- })
685
-
686
680
  if (Object.keys(clientHydrateInput).length > 0) {
687
- const clientBundle = await rollup({
688
- input: clientHydrateInput,
689
- plugins: clientPlugins,
690
- })
691
- await clientBundle.write({
692
- dir: buildPath,
693
- format: 'esm',
694
- entryFileNames ({ name }) {
695
- if (name.startsWith('hydrate__')) {
696
- const pageId = name.slice('hydrate__'.length)
697
- return `public/static/hydrate-${pageId}.js`
698
- }
699
- return 'public/static/[name].js'
700
- },
701
- chunkFileNames: 'public/static/[name]-[hash].js',
702
- plugins: [minifyBrowserStaticChunks()],
703
- })
704
- await clientBundle.close()
705
- }
706
-
707
- if (pageFiles.length > 0) {
708
- await writePrerenderedPages({
681
+ await prerenderReactTask.handler({
682
+ op: 'runProduction',
683
+ clientHydrateInput,
684
+ pageFilesLength: pageFiles.length,
685
+ copyPublicFrom: publicDir,
686
+ buildPath,
687
+ nodeEnv: 'production',
709
688
  pagesBundlePath,
710
689
  configSourcePath,
711
- publicDir: path.join(buildPath, 'public'),
690
+ createClientRollupPlugins: createOssyClientRollupPlugins,
691
+ minifyBrowserStaticChunks,
712
692
  })
713
693
  }
714
694
 
package/cli/dev.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  resourceTemplatesDir,
29
29
  OSSY_RESOURCE_TEMPLATES_OUT,
30
30
  } from './build.js';
31
- import { writePrerenderedPages } from './prerender-pages.js'
31
+ import prerenderReactTask from './prerender-react.task.js'
32
32
  import { watch } from 'rollup';
33
33
  import arg from 'arg'
34
34
  import { spawn } from 'node:child_process'
@@ -241,7 +241,8 @@ export const dev = async (cliArgs) => {
241
241
  copyOssyAppRuntime({ scriptDir, buildPath })
242
242
  if (pageFiles.length > 0) {
243
243
  try {
244
- await writePrerenderedPages({
244
+ await prerenderReactTask.handler({
245
+ op: 'prerenderPagesParallel',
245
246
  pagesBundlePath,
246
247
  configSourcePath,
247
248
  publicDir: path.join(buildPath, 'public'),
@@ -0,0 +1,270 @@
1
+ import path from 'path'
2
+ import url from 'url'
3
+ import fs from 'fs'
4
+ import { rollup } from 'rollup'
5
+ import { BuildPage } from './render-page.task.js'
6
+
7
+ /**
8
+ * Maps an app route path to the file path express.static will serve for that URL
9
+ * (`/` → `public/index.html`, `/a/b` → `public/a/b/index.html`).
10
+ */
11
+ export function staticHtmlPathForRoute (routePath, publicDir) {
12
+ const p = typeof routePath === 'string' ? routePath : '/'
13
+ if (p === '/' || p === '') {
14
+ return path.join(publicDir, 'index.html')
15
+ }
16
+ const segments = p.replace(/^\//, '').split('/').filter(Boolean)
17
+ return path.join(publicDir, ...segments, 'index.html')
18
+ }
19
+
20
+ function pathIsPrerenderable (routePath) {
21
+ if (typeof routePath !== 'string') return false
22
+ if (!routePath.startsWith('/') || routePath.includes(':')) return false
23
+ return true
24
+ }
25
+
26
+ /** Mirrors server-era app shell config with static defaults (no cookies / session). */
27
+ export function buildPrerenderAppConfig ({
28
+ buildTimeConfig,
29
+ pageList,
30
+ activeRouteId,
31
+ urlPath,
32
+ }) {
33
+ const pages = pageList.map((page) => {
34
+ const entry = {
35
+ id: page?.id,
36
+ path: page?.path,
37
+ }
38
+ if (activeRouteId != null && page?.id === activeRouteId) {
39
+ entry.element = page?.element
40
+ }
41
+ return entry
42
+ })
43
+ return {
44
+ ...buildTimeConfig,
45
+ url: urlPath,
46
+ theme: buildTimeConfig.theme || 'light',
47
+ isAuthenticated: false,
48
+ workspaceId: buildTimeConfig.workspaceId,
49
+ apiUrl: buildTimeConfig.apiUrl,
50
+ pages,
51
+ sidebarPrimaryCollapsed: false,
52
+ }
53
+ }
54
+
55
+ function copyPublicToBuild ({ copyPublicFrom, buildPath }) {
56
+ if (!copyPublicFrom || !fs.existsSync(copyPublicFrom)) return
57
+ const dest = path.join(buildPath, 'public')
58
+ fs.mkdirSync(dest, { recursive: true })
59
+ fs.cpSync(copyPublicFrom, dest, { recursive: true })
60
+ }
61
+
62
+ function countSettledFailures (results) {
63
+ return results.filter((r) => r.status === 'rejected').length
64
+ }
65
+
66
+ async function bundleOneHydratePage ({
67
+ entryName,
68
+ stubPath,
69
+ buildPath,
70
+ plugins,
71
+ minifyPlugin,
72
+ }) {
73
+ const bundle = await rollup({
74
+ input: { [entryName]: stubPath },
75
+ plugins,
76
+ })
77
+ try {
78
+ await bundle.write({
79
+ dir: buildPath,
80
+ format: 'esm',
81
+ inlineDynamicImports: true,
82
+ entryFileNames (chunkInfo) {
83
+ const n = chunkInfo.name
84
+ if (n.startsWith('hydrate__')) {
85
+ const pageId = n.slice('hydrate__'.length)
86
+ return `public/static/hydrate-${pageId}.js`
87
+ }
88
+ return 'public/static/[name].js'
89
+ },
90
+ chunkFileNames: 'public/static/[name]-[hash].js',
91
+ plugins: minifyPlugin ? [minifyPlugin] : [],
92
+ })
93
+ } finally {
94
+ await bundle.close()
95
+ }
96
+ }
97
+
98
+ async function bundleHydratePagesParallel ({
99
+ clientHydrateInput,
100
+ buildPath,
101
+ copyPublicFrom,
102
+ nodeEnv,
103
+ buildPathForPlugins,
104
+ createClientRollupPlugins,
105
+ minifyBrowserStaticChunks,
106
+ }) {
107
+ copyPublicToBuild({ copyPublicFrom, buildPath })
108
+ fs.mkdirSync(path.join(buildPath, 'public', 'static'), { recursive: true })
109
+
110
+ const entries = Object.entries(clientHydrateInput)
111
+
112
+ const results = await Promise.allSettled(
113
+ entries.map(([entryName, stubPath]) => {
114
+ const plugins = createClientRollupPlugins({
115
+ nodeEnv,
116
+ copyPublicFrom: undefined,
117
+ buildPath: buildPathForPlugins,
118
+ })
119
+ return bundleOneHydratePage({
120
+ entryName,
121
+ stubPath,
122
+ buildPath,
123
+ plugins,
124
+ minifyPlugin: minifyBrowserStaticChunks(),
125
+ })
126
+ })
127
+ )
128
+
129
+ const failures = countSettledFailures(results)
130
+ results.forEach((r, i) => {
131
+ if (r.status === 'rejected') {
132
+ const [entryName] = entries[i]
133
+ console.error(`[@ossy/app][client-bundle] ${entryName} failed:`, r.reason)
134
+ }
135
+ })
136
+
137
+ return { results, failures }
138
+ }
139
+
140
+ async function prerenderOnePage ({
141
+ route,
142
+ buildTimeConfig,
143
+ pageList,
144
+ publicDir,
145
+ }) {
146
+ const appConfig = buildPrerenderAppConfig({
147
+ buildTimeConfig,
148
+ pageList,
149
+ activeRouteId: route.id,
150
+ urlPath: route.path,
151
+ })
152
+ const html = await BuildPage.handle({
153
+ route,
154
+ appConfig,
155
+ isDevReloadEnabled: false,
156
+ })
157
+ const outPath = staticHtmlPathForRoute(route.path, publicDir)
158
+ fs.mkdirSync(path.dirname(outPath), { recursive: true })
159
+ fs.writeFileSync(outPath, html, 'utf8')
160
+ console.log(
161
+ `[@ossy/app][prerender] ${route.path} → ${path.relative(process.cwd(), outPath)}`
162
+ )
163
+ }
164
+
165
+ async function prerenderPagesParallel ({
166
+ pagesBundlePath,
167
+ configSourcePath,
168
+ publicDir,
169
+ }) {
170
+ const cfgHref = url.pathToFileURL(path.resolve(configSourcePath)).href
171
+ const pagesHref = url.pathToFileURL(path.resolve(pagesBundlePath)).href
172
+
173
+ const configModule = await import(cfgHref)
174
+ const pagesModule = await import(pagesHref)
175
+
176
+ const buildTimeConfig = configModule?.default ?? configModule ?? {}
177
+ const pageList = pagesModule?.default ?? []
178
+
179
+ const routesToRender = []
180
+ for (const route of pageList) {
181
+ if (!route?.element) continue
182
+ if (!pathIsPrerenderable(route.path)) {
183
+ console.warn(
184
+ `[@ossy/app][prerender] Skipping "${route.id}" (path not prerenderable: ${JSON.stringify(route.path)})`
185
+ )
186
+ continue
187
+ }
188
+ routesToRender.push(route)
189
+ }
190
+
191
+ const results = await Promise.allSettled(
192
+ routesToRender.map((route) =>
193
+ prerenderOnePage({
194
+ route,
195
+ buildTimeConfig,
196
+ pageList,
197
+ publicDir,
198
+ })
199
+ )
200
+ )
201
+ const failures = countSettledFailures(results)
202
+ results.forEach((r, i) => {
203
+ if (r.status === 'rejected') {
204
+ const route = routesToRender[i]
205
+ console.error(
206
+ `[@ossy/app][prerender] "${route?.id}" (${route?.path}) failed:`,
207
+ r.reason
208
+ )
209
+ }
210
+ })
211
+
212
+ return { results, failures }
213
+ }
214
+
215
+ async function runProduction ({
216
+ clientHydrateInput,
217
+ pageFilesLength,
218
+ copyPublicFrom,
219
+ buildPath,
220
+ nodeEnv,
221
+ pagesBundlePath,
222
+ configSourcePath,
223
+ createClientRollupPlugins,
224
+ minifyBrowserStaticChunks,
225
+ }) {
226
+ if (pageFilesLength === 0 || Object.keys(clientHydrateInput).length === 0) {
227
+ return { bundleFailures: 0, prerenderFailures: 0 }
228
+ }
229
+
230
+ const { failures: bundleFailures } = await bundleHydratePagesParallel({
231
+ clientHydrateInput,
232
+ buildPath,
233
+ copyPublicFrom,
234
+ nodeEnv,
235
+ buildPathForPlugins: buildPath,
236
+ createClientRollupPlugins,
237
+ minifyBrowserStaticChunks,
238
+ })
239
+
240
+ const { failures: prerenderFailures } = await prerenderPagesParallel({
241
+ pagesBundlePath,
242
+ configSourcePath,
243
+ publicDir: path.join(buildPath, 'public'),
244
+ })
245
+
246
+ if (bundleFailures + prerenderFailures > 0) {
247
+ console.warn(
248
+ `[@ossy/app][build] Finished with ${bundleFailures} client bundle error(s) and ${prerenderFailures} prerender error(s)`
249
+ )
250
+ }
251
+
252
+ return { bundleFailures, prerenderFailures }
253
+ }
254
+
255
+ export default {
256
+ type: '@ossy/app/prerender-react',
257
+ /** `input.op`: `runProduction` | `prerenderPagesParallel` (see callers in `build.js` / `dev.js`). */
258
+ async handler (input) {
259
+ const op = input?.op
260
+ if (op === 'runProduction') {
261
+ return runProduction(input)
262
+ }
263
+ if (op === 'prerenderPagesParallel') {
264
+ return prerenderPagesParallel(input)
265
+ }
266
+ throw new Error(
267
+ `[@ossy/app][prerender-react] Unknown op: ${String(op)} (expected runProduction | prerenderPagesParallel)`
268
+ )
269
+ },
270
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
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.9.0",
31
- "@ossy/design-system": "^1.9.0",
32
- "@ossy/pages": "^1.9.0",
33
- "@ossy/router": "^1.9.0",
34
- "@ossy/router-react": "^1.9.0",
35
- "@ossy/sdk": "^1.9.0",
36
- "@ossy/sdk-react": "^1.9.0",
37
- "@ossy/themes": "^1.9.0",
30
+ "@ossy/connected-components": "^1.10.0",
31
+ "@ossy/design-system": "^1.10.0",
32
+ "@ossy/pages": "^1.10.0",
33
+ "@ossy/router": "^1.10.0",
34
+ "@ossy/router-react": "^1.10.0",
35
+ "@ossy/sdk": "^1.10.0",
36
+ "@ossy/sdk-react": "^1.10.0",
37
+ "@ossy/themes": "^1.10.0",
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": "9be5f444e4cd530e29be0193fc0e7fd8da0bc995"
70
+ "gitHead": "bd9dfaf87661392562fbe2cdfd3ca6d645147f24"
71
71
  }
@@ -1,101 +0,0 @@
1
- import path from 'path'
2
- import url from 'url'
3
- import fs from 'fs'
4
- import { BuildPage } from './render-page.task.js'
5
-
6
- /**
7
- * Maps an app route path to the file path express.static will serve for that URL
8
- * (`/` → `public/index.html`, `/a/b` → `public/a/b/index.html`).
9
- */
10
- export function staticHtmlPathForRoute (routePath, publicDir) {
11
- const p = typeof routePath === 'string' ? routePath : '/'
12
- if (p === '/' || p === '') {
13
- return path.join(publicDir, 'index.html')
14
- }
15
- const segments = p.replace(/^\//, '').split('/').filter(Boolean)
16
- return path.join(publicDir, ...segments, 'index.html')
17
- }
18
-
19
- function pathIsPrerenderable (routePath) {
20
- if (typeof routePath !== 'string') return false
21
- if (!routePath.startsWith('/') || routePath.includes(':')) return false
22
- return true
23
- }
24
-
25
- /** Mirrors server `resolveAppConfig` with static defaults (no cookies / session). */
26
- export function buildPrerenderAppConfig ({
27
- buildTimeConfig,
28
- pageList,
29
- activeRouteId,
30
- urlPath,
31
- }) {
32
- const pages = pageList.map((page) => {
33
- const entry = {
34
- id: page?.id,
35
- path: page?.path,
36
- }
37
- if (activeRouteId != null && page?.id === activeRouteId) {
38
- entry.element = page?.element
39
- }
40
- return entry
41
- })
42
- return {
43
- ...buildTimeConfig,
44
- url: urlPath,
45
- theme: buildTimeConfig.theme || 'light',
46
- isAuthenticated: false,
47
- workspaceId: buildTimeConfig.workspaceId,
48
- apiUrl: buildTimeConfig.apiUrl,
49
- pages,
50
- sidebarPrimaryCollapsed: false,
51
- }
52
- }
53
-
54
- /**
55
- * After client bundles and `public/` exist, writes one HTML file per static page
56
- * so express.static can serve them without runtime React rendering.
57
- */
58
- export async function writePrerenderedPages ({
59
- pagesBundlePath,
60
- configSourcePath,
61
- publicDir,
62
- }) {
63
- const cfgHref = url.pathToFileURL(path.resolve(configSourcePath)).href
64
- const pagesHref = url.pathToFileURL(path.resolve(pagesBundlePath)).href
65
-
66
- const configModule = await import(cfgHref)
67
- const pagesModule = await import(pagesHref)
68
-
69
- const buildTimeConfig = configModule?.default ?? configModule ?? {}
70
- const pageList = pagesModule?.default ?? []
71
-
72
- for (const route of pageList) {
73
- if (!route?.element) continue
74
- if (!pathIsPrerenderable(route.path)) {
75
- console.warn(
76
- `[@ossy/app][prerender] Skipping "${route.id}" (path not prerenderable: ${JSON.stringify(route.path)})`
77
- )
78
- continue
79
- }
80
-
81
- const appConfig = buildPrerenderAppConfig({
82
- buildTimeConfig,
83
- pageList,
84
- activeRouteId: route.id,
85
- urlPath: route.path,
86
- })
87
-
88
- const html = await BuildPage.handle({
89
- route,
90
- appConfig,
91
- isDevReloadEnabled: false,
92
- })
93
-
94
- const outPath = staticHtmlPathForRoute(route.path, publicDir)
95
- fs.mkdirSync(path.dirname(outPath), { recursive: true })
96
- fs.writeFileSync(outPath, html, 'utf8')
97
- console.log(
98
- `[@ossy/app][prerender] ${route.path} → ${path.relative(process.cwd(), outPath)}`
99
- )
100
- }
101
- }