@ossy/app 1.39.4 → 1.39.6

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 CHANGED
@@ -227,7 +227,7 @@ function generateE2eStub ({ stubAbs, sourceAbs }) {
227
227
  }
228
228
 
229
229
  // Layouts are React components that wrap page renders. The stub re-exports
230
- // everything (including the optional `applies` glob and required `id`) plus the
230
+ // everything (including required `id`) plus the
231
231
  // default component so the manifest plugin can validate them and the server
232
232
  // can inject them at render time.
233
233
  function generateLayoutStub ({ stubAbs, sourceAbs }) {
@@ -73,7 +73,6 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
73
73
  * @typedef {object} LayoutManifestEntry
74
74
  * @property {string} id Unique layout id (e.g. `'app-shell'`).
75
75
  * @property {string} entry URL the platform serves the layout bundle from.
76
- * @property {string} [applies] Optional glob pattern of page paths this layout applies to (default `'*'` = all pages).
77
76
  *
78
77
  * @typedef {object} E2eManifestEntry
79
78
  * @property {string} id Unique test id (e.g. `'authentication/sign-in'`).
@@ -346,11 +345,10 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
346
345
  continue
347
346
  }
348
347
 
349
- // Layouts wrap page renders automatically. The bundle exports `id`
350
- // (a unique slug), an optional `applies` glob, and a default export
351
- // (the React component). They don't produce routable entries; they
352
- // accumulate into a separate `layouts` array so the server can
353
- // match and inject them at render time.
348
+ // Layouts wrap every page render. The bundle exports `id` and a default
349
+ // export (the React component). Only one `*.layout.jsx` per app is
350
+ // supported. They don't produce routable entries; they accumulate into
351
+ // `manifest.layouts` so the server can inject them at render time.
354
352
  if (entryInfo.kind === 'layout') {
355
353
  const layoutId = mod && mod.id
356
354
  const component = mod && mod.default
@@ -368,8 +366,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
368
366
  this.error(`[@ossy/app][build] Duplicate layout id "${layoutId}"`)
369
367
  }
370
368
  seenLayoutIds.add(layoutId)
371
- const applies = typeof mod.applies === 'string' ? mod.applies : '*'
372
- layouts.push({ id: layoutId, entry: url, applies })
369
+ layouts.push({ id: layoutId, entry: url })
373
370
  continue
374
371
  }
375
372
 
@@ -419,7 +416,13 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
419
416
 
420
417
  if (entryInfo.kind === 'page') {
421
418
  const pagePath = rawMeta.path !== undefined ? rawMeta.path : defaultPageRoute(id)
422
- entries.push({ type: 'page', id, path: pagePath, title: rawMeta.title, entry: url })
419
+ entries.push({
420
+ type: 'page',
421
+ id,
422
+ path: pagePath,
423
+ title: rawMeta.title,
424
+ entry: url,
425
+ })
423
426
  } else if (entryInfo.kind === 'api') {
424
427
  // APIs have no sensible default route (unlike pages where we can
425
428
  // derive `/<id>`). Failing here surfaces a missing `metadata.path`
@@ -438,6 +441,13 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
438
441
  }
439
442
  }
440
443
 
444
+ if (layouts.length > 1) {
445
+ this.error(
446
+ `[@ossy/app][build] Only one *.layout.jsx per app is allowed (found ${layouts.length}). ` +
447
+ 'Use conditional rendering inside the app layout for route-specific chrome.',
448
+ )
449
+ }
450
+
441
451
  /** @type {Manifest} */
442
452
  const manifest = { entries, components, slots, resourceTemplates, aggregates, integrations, startups, emails, actions, e2es, layouts, config: configValue || {} }
443
453
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "1.39.4",
3
+ "version": "1.39.6",
4
4
  "description": "",
5
5
  "source": "./src/index.js",
6
6
  "main": "./src/index.js",
@@ -38,14 +38,14 @@
38
38
  "@babel/eslint-parser": "^7.15.8",
39
39
  "@babel/preset-react": "^7.26.3",
40
40
  "@babel/register": "^7.25.9",
41
- "@ossy/design-system": "^1.39.4",
41
+ "@ossy/design-system": "^1.39.6",
42
42
  "@ossy/pages": "^1.23.0",
43
- "@ossy/platform": "^1.38.4",
44
- "@ossy/router": "^1.39.4",
45
- "@ossy/router-react": "^1.39.4",
46
- "@ossy/sdk": "^1.39.4",
47
- "@ossy/sdk-react": "^1.39.4",
48
- "@ossy/themes": "^1.39.4",
43
+ "@ossy/platform": "^1.38.6",
44
+ "@ossy/router": "^1.39.6",
45
+ "@ossy/router-react": "^1.39.6",
46
+ "@ossy/sdk": "^1.39.6",
47
+ "@ossy/sdk-react": "^1.39.6",
48
+ "@ossy/themes": "^1.39.6",
49
49
  "@rollup/plugin-alias": "^6.0.0",
50
50
  "@rollup/plugin-babel": "^7.0.0",
51
51
  "@rollup/plugin-commonjs": "^29.0.0",
@@ -79,5 +79,5 @@
79
79
  "README.md",
80
80
  "tsconfig.json"
81
81
  ],
82
- "gitHead": "188d15e1d90f102bc31996da3b32a5aef213f4b7"
82
+ "gitHead": "72d150cf20ff7ba25b328bc5b429b065c2840b2b"
83
83
  }
@@ -26,6 +26,25 @@ export async function loadComponents (entries = []) {
26
26
  return map
27
27
  }
28
28
 
29
+ /**
30
+ * Dynamically imports the app's single layout bundle. Layout components cannot
31
+ * be serialized into the bootstrap JSON (functions are stripped), so the server
32
+ * passes `layoutEntry` and the client loads the same module here. When present,
33
+ * every page is wrapped — route-specific chrome is decided inside the layout.
34
+ *
35
+ * @param {string | null | undefined} layoutEntry
36
+ * @returns {Promise<import('react').ComponentType | null>}
37
+ */
38
+ export async function loadLayout (layoutEntry) {
39
+ if (!layoutEntry) return null
40
+ try {
41
+ const mod = await import(layoutEntry)
42
+ return mod?.default ?? null
43
+ } catch {
44
+ return null
45
+ }
46
+ }
47
+
29
48
  function buildTree ({ Component, Layout, metadata, props }) {
30
49
  const lang = props.htmlLang || props.defaultLanguage || 'en'
31
50
  const pageEl = createElement(Component, props)
@@ -82,8 +101,11 @@ export function createPageEntry (pageModule, options = {}) {
82
101
  const tree = buildTree({ Component, Layout, metadata, props: { ...pageProps, components } })
83
102
  const bootstrapUrl = toBootstrapUrl(entryUrl)
84
103
  const bootstrapModules = bootstrapUrl ? [bootstrapUrl] : []
104
+ // Layout is a React component — JSON.stringify drops functions, so hydration
105
+ // would render without the shell unless we pass the serializable entry URL.
106
+ const { Layout: _layout, ...bootstrapProps } = props
85
107
  const bootstrapScriptContent =
86
- 'window.__OSSY__=' + escapeBootstrapJson(JSON.stringify(props ?? {}))
108
+ 'window.__OSSY__=' + escapeBootstrapJson(JSON.stringify(bootstrapProps ?? {}))
87
109
 
88
110
  return new Promise((resolve, reject) => {
89
111
  let html = ''
@@ -111,11 +133,12 @@ export function createPageEntry (pageModule, options = {}) {
111
133
  if (typeof document === 'undefined' || typeof window === 'undefined') return
112
134
  hydrated = true
113
135
  const props = window.__OSSY__ || {}
114
- const { Layout = null, componentEntries = [], ...pageProps } = props
136
+ const { layoutEntry = null, componentEntries = [], ...pageProps } = props
115
137
  Promise.all([
116
138
  import('react-dom/client'),
117
139
  loadComponents(componentEntries),
118
- ]).then(([{ hydrateRoot }, components]) => {
140
+ loadLayout(layoutEntry),
141
+ ]).then(([{ hydrateRoot }, components, Layout]) => {
119
142
  hydrateRoot(document, buildTree({ Component, Layout, metadata, props: { ...pageProps, components } }))
120
143
  })
121
144
  }