@ossy/app 1.37.1 → 1.37.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 +19 -2
- package/cli/get-platform-files.task.js +8 -3
- package/cli/manifest-plugin.js +37 -2
- package/package.json +9 -9
- package/runtime/page-runtime.js +8 -6
package/cli/build.task.js
CHANGED
|
@@ -21,10 +21,11 @@ import getPlatformFiles, {
|
|
|
21
21
|
EMAIL_FILE_PATTERN,
|
|
22
22
|
ACTION_FILE_PATTERN,
|
|
23
23
|
E2E_FILE_PATTERN,
|
|
24
|
+
LAYOUT_FILE_PATTERN,
|
|
24
25
|
} from './get-platform-files.task.js'
|
|
25
26
|
import { manifestPlugin } from './manifest-plugin.js'
|
|
26
27
|
|
|
27
|
-
export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, RESOURCE_FILE_PATTERN, COMPONENT_FILE_PATTERN, AGGREGATE_FILE_PATTERN, INTEGRATION_FILE_PATTERN, STARTUP_FILE_PATTERN, EMAIL_FILE_PATTERN, ACTION_FILE_PATTERN, E2E_FILE_PATTERN }
|
|
28
|
+
export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, RESOURCE_FILE_PATTERN, COMPONENT_FILE_PATTERN, AGGREGATE_FILE_PATTERN, INTEGRATION_FILE_PATTERN, STARTUP_FILE_PATTERN, EMAIL_FILE_PATTERN, ACTION_FILE_PATTERN, E2E_FILE_PATTERN, LAYOUT_FILE_PATTERN }
|
|
28
29
|
|
|
29
30
|
// Generated entry stubs live under `build/.ossy/entries/`. Putting them
|
|
30
31
|
// inside `build/` means they're cleaned automatically with every build,
|
|
@@ -74,7 +75,7 @@ function entryNameFromSource (sourcePath, srcDir, packageName) {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
function stubFileNameFor (kind, sourcePath, srcDir, packageName) {
|
|
77
|
-
const ext = (kind === 'page' || kind === 'component' || kind === 'email') ? '.entry.jsx' : '.entry.js'
|
|
78
|
+
const ext = (kind === 'page' || kind === 'component' || kind === 'email' || kind === 'layout') ? '.entry.jsx' : '.entry.js'
|
|
78
79
|
return entryNameFromSource(sourcePath, srcDir, packageName) + ext
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -218,6 +219,20 @@ function generateE2eStub ({ stubAbs, sourceAbs }) {
|
|
|
218
219
|
].join('\n')
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
// Layouts are React components that wrap page renders. The stub re-exports
|
|
223
|
+
// everything (including the optional `applies` glob and required `id`) plus the
|
|
224
|
+
// default component so the manifest plugin can validate them and the server
|
|
225
|
+
// can inject them at render time.
|
|
226
|
+
function generateLayoutStub ({ stubAbs, sourceAbs }) {
|
|
227
|
+
const importPath = relImport(stubAbs, sourceAbs)
|
|
228
|
+
return [
|
|
229
|
+
'// Generated by @ossy/app — do not edit',
|
|
230
|
+
`export * from '${importPath}'`,
|
|
231
|
+
`export { default } from '${importPath}'`,
|
|
232
|
+
'',
|
|
233
|
+
].join('\n')
|
|
234
|
+
}
|
|
235
|
+
|
|
221
236
|
function stubFor (kind, args) {
|
|
222
237
|
if (kind === 'page') return generatePageStub(args)
|
|
223
238
|
if (kind === 'api') return generateApiStub(args)
|
|
@@ -229,6 +244,7 @@ function stubFor (kind, args) {
|
|
|
229
244
|
if (kind === 'email') return generateEmailStub(args)
|
|
230
245
|
if (kind === 'action') return generateActionStub(args)
|
|
231
246
|
if (kind === 'e2e') return generateE2eStub(args)
|
|
247
|
+
if (kind === 'layout') return generateLayoutStub(args)
|
|
232
248
|
return generateTaskStub(args)
|
|
233
249
|
}
|
|
234
250
|
|
|
@@ -276,6 +292,7 @@ export async function build (cliArgs = []) {
|
|
|
276
292
|
...platformFiles.emails,
|
|
277
293
|
...platformFiles.actions,
|
|
278
294
|
...platformFiles.e2es,
|
|
295
|
+
...platformFiles.layouts,
|
|
279
296
|
]
|
|
280
297
|
|
|
281
298
|
const entriesByStub = new Map()
|
|
@@ -12,9 +12,10 @@ export const STARTUP_FILE_PATTERN = /\.startup\.(mjs|cjs|js)$/
|
|
|
12
12
|
export const EMAIL_FILE_PATTERN = /\.email\.(jsx?|tsx?)$/
|
|
13
13
|
export const ACTION_FILE_PATTERN = /\.action\.(mjs|cjs|js)$/
|
|
14
14
|
export const E2E_FILE_PATTERN = /\.e2e\.(mjs|cjs|js)$/
|
|
15
|
+
export const LAYOUT_FILE_PATTERN = /\.layout\.(jsx?|tsx?)$/
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* @typedef {'page' | 'api' | 'task' | 'resource' | 'component' | 'aggregate' | 'integration' | 'startup' | 'email' | 'action' | 'e2e'} EntryKind
|
|
18
|
+
* @typedef {'page' | 'api' | 'task' | 'resource' | 'component' | 'aggregate' | 'integration' | 'startup' | 'email' | 'action' | 'e2e' | 'layout'} EntryKind
|
|
18
19
|
*
|
|
19
20
|
* @typedef {object} PlatformEntry
|
|
20
21
|
* @property {EntryKind} kind Which bucket this entry lives in.
|
|
@@ -34,6 +35,7 @@ export const E2E_FILE_PATTERN = /\.e2e\.(mjs|cjs|js)$/
|
|
|
34
35
|
* @property {PlatformEntry[]} emails Discovered `*.email.{jsx,tsx,...}` entries.
|
|
35
36
|
* @property {PlatformEntry[]} actions Discovered `*.action.{js,mjs,cjs}` entries.
|
|
36
37
|
* @property {PlatformEntry[]} e2es Discovered `*.e2e.{js,mjs,cjs}` entries (installed packages only).
|
|
38
|
+
* @property {PlatformEntry[]} layouts Discovered `*.layout.{jsx,tsx,...}` entries.
|
|
37
39
|
*/
|
|
38
40
|
export function discoverFilesByPattern (srcDir, filePattern) {
|
|
39
41
|
const dir = path.resolve(srcDir)
|
|
@@ -63,6 +65,7 @@ function classifyFile (absPath) {
|
|
|
63
65
|
if (EMAIL_FILE_PATTERN.test(base)) return 'email'
|
|
64
66
|
if (ACTION_FILE_PATTERN.test(base)) return 'action'
|
|
65
67
|
if (E2E_FILE_PATTERN.test(base)) return 'e2e'
|
|
68
|
+
if (LAYOUT_FILE_PATTERN.test(base)) return 'layout'
|
|
66
69
|
return null
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -138,6 +141,7 @@ export function discoverInstalledPackageEntries (projectRoot) {
|
|
|
138
141
|
...discoverFilesByPattern(packageSrcDir, EMAIL_FILE_PATTERN),
|
|
139
142
|
...discoverFilesByPattern(packageSrcDir, ACTION_FILE_PATTERN),
|
|
140
143
|
...discoverFilesByPattern(packageSrcDir, E2E_FILE_PATTERN),
|
|
144
|
+
...discoverFilesByPattern(packageSrcDir, LAYOUT_FILE_PATTERN),
|
|
141
145
|
]
|
|
142
146
|
for (const sourcePath of files) {
|
|
143
147
|
const stat = fs.statSync(sourcePath)
|
|
@@ -181,8 +185,8 @@ export function discoverInstalledPackageEntries (projectRoot) {
|
|
|
181
185
|
* @returns {Promise<PlatformFiles>}
|
|
182
186
|
*/
|
|
183
187
|
export default async function getPlatformFiles (srcDir) {
|
|
184
|
-
const out = { pages: [], apis: [], tasks: [], resources: [], components: [], aggregates: [], integrations: [], startups: [], emails: [], actions: [], e2es: [] }
|
|
185
|
-
const bucketByKind = { page: 'pages', api: 'apis', task: 'tasks', resource: 'resources', component: 'components', aggregate: 'aggregates', integration: 'integrations', startup: 'startups', email: 'emails', action: 'actions', e2e: 'e2es' }
|
|
188
|
+
const out = { pages: [], apis: [], tasks: [], resources: [], components: [], aggregates: [], integrations: [], startups: [], emails: [], actions: [], e2es: [], layouts: [] }
|
|
189
|
+
const bucketByKind = { page: 'pages', api: 'apis', task: 'tasks', resource: 'resources', component: 'components', aggregate: 'aggregates', integration: 'integrations', startup: 'startups', email: 'emails', action: 'actions', e2e: 'e2es', layout: 'layouts' }
|
|
186
190
|
|
|
187
191
|
// Aggregates and e2e tests are only discovered from installed packages — never
|
|
188
192
|
// from the local `src/` — to avoid pulling in per-project duplicates of the
|
|
@@ -198,6 +202,7 @@ export default async function getPlatformFiles (srcDir) {
|
|
|
198
202
|
...discoverFilesByPattern(srcDir, STARTUP_FILE_PATTERN),
|
|
199
203
|
...discoverFilesByPattern(srcDir, EMAIL_FILE_PATTERN),
|
|
200
204
|
...discoverFilesByPattern(srcDir, ACTION_FILE_PATTERN),
|
|
205
|
+
...discoverFilesByPattern(srcDir, LAYOUT_FILE_PATTERN),
|
|
201
206
|
]
|
|
202
207
|
for (const sourcePath of localFiles) {
|
|
203
208
|
const stat = fs.statSync(sourcePath)
|
package/cli/manifest-plugin.js
CHANGED
|
@@ -64,6 +64,11 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
|
|
|
64
64
|
* @property {string} entry URL the platform serves the action bundle from.
|
|
65
65
|
* @property {string} access Access level: `'public'` | `'authenticated'` | `'workspace'`.
|
|
66
66
|
*
|
|
67
|
+
* @typedef {object} LayoutManifestEntry
|
|
68
|
+
* @property {string} id Unique layout id (e.g. `'app-shell'`).
|
|
69
|
+
* @property {string} entry URL the platform serves the layout bundle from.
|
|
70
|
+
* @property {string} [applies] Optional glob pattern of page paths this layout applies to (default `'*'` = all pages).
|
|
71
|
+
*
|
|
67
72
|
* @typedef {object} E2eManifestEntry
|
|
68
73
|
* @property {string} id Unique test id (e.g. `'authentication/sign-in'`).
|
|
69
74
|
* @property {string} feature Grouping label (e.g. `'authentication'`).
|
|
@@ -80,6 +85,7 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
|
|
|
80
85
|
* @property {EmailManifestEntry[]} emails Discovered `*.email.{jsx,tsx}` entries.
|
|
81
86
|
* @property {ActionManifestEntry[]} actions Discovered `*.action.js` entries.
|
|
82
87
|
* @property {E2eManifestEntry[]} e2es Discovered `*.e2e.js` entries from installed packages.
|
|
88
|
+
* @property {LayoutManifestEntry[]} layouts Discovered `*.layout.{jsx,tsx}` entries.
|
|
83
89
|
* @property {object} config Inlined `src/config.js` default export.
|
|
84
90
|
*/
|
|
85
91
|
|
|
@@ -147,7 +153,9 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
147
153
|
/** @type {E2eManifestEntry[]} */
|
|
148
154
|
const e2es = []
|
|
149
155
|
const seenE2eIds = new Set()
|
|
150
|
-
|
|
156
|
+
/** @type {LayoutManifestEntry[]} */
|
|
157
|
+
const layouts = []
|
|
158
|
+
const seenLayoutIds = new Set()
|
|
151
159
|
for (const fileName of Object.keys(bundle)) {
|
|
152
160
|
const chunk = bundle[fileName]
|
|
153
161
|
if (chunk.type !== 'chunk' || !chunk.isEntry) continue
|
|
@@ -329,6 +337,33 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
329
337
|
continue
|
|
330
338
|
}
|
|
331
339
|
|
|
340
|
+
// Layouts wrap page renders automatically. The bundle exports `id`
|
|
341
|
+
// (a unique slug), an optional `applies` glob, and a default export
|
|
342
|
+
// (the React component). They don't produce routable entries; they
|
|
343
|
+
// accumulate into a separate `layouts` array so the server can
|
|
344
|
+
// match and inject them at render time.
|
|
345
|
+
if (entryInfo.kind === 'layout') {
|
|
346
|
+
const layoutId = mod && mod.id
|
|
347
|
+
const component = mod && mod.default
|
|
348
|
+
if (typeof layoutId !== 'string' || layoutId.trim() === '') {
|
|
349
|
+
this.error(
|
|
350
|
+
`[@ossy/app][build] layout entry ${entryInfo.sourcePath} must export a non-empty string "id"`,
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
if (typeof component !== 'function') {
|
|
354
|
+
this.error(
|
|
355
|
+
`[@ossy/app][build] layout entry ${entryInfo.sourcePath} must default-export a React component function`,
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
if (seenLayoutIds.has(layoutId)) {
|
|
359
|
+
this.error(`[@ossy/app][build] Duplicate layout id "${layoutId}"`)
|
|
360
|
+
}
|
|
361
|
+
seenLayoutIds.add(layoutId)
|
|
362
|
+
const applies = typeof mod.applies === 'string' ? mod.applies : '*'
|
|
363
|
+
layouts.push({ id: layoutId, entry: url, applies })
|
|
364
|
+
continue
|
|
365
|
+
}
|
|
366
|
+
|
|
332
367
|
// Resources are pure data — the default export *is* the template.
|
|
333
368
|
// They don't use `metadata`, don't need a derived id, and don't
|
|
334
369
|
// produce a routable manifest entry; they accumulate into a separate
|
|
@@ -390,7 +425,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
390
425
|
}
|
|
391
426
|
|
|
392
427
|
/** @type {Manifest} */
|
|
393
|
-
const manifest = { entries, components, resourceTemplates, aggregates, integrations, startups, emails, actions, e2es, config: configValue || {} }
|
|
428
|
+
const manifest = { entries, components, resourceTemplates, aggregates, integrations, startups, emails, actions, e2es, layouts, config: configValue || {} }
|
|
394
429
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
|
|
395
430
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
|
|
396
431
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/app",
|
|
3
|
-
"version": "1.37.
|
|
3
|
+
"version": "1.37.2",
|
|
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.37.
|
|
41
|
+
"@ossy/design-system": "^1.37.2",
|
|
42
42
|
"@ossy/pages": "^1.23.0",
|
|
43
|
-
"@ossy/platform": "^1.36.
|
|
44
|
-
"@ossy/router": "^1.37.
|
|
45
|
-
"@ossy/router-react": "^1.37.
|
|
46
|
-
"@ossy/sdk": "^1.37.
|
|
47
|
-
"@ossy/sdk-react": "^1.37.
|
|
48
|
-
"@ossy/themes": "^1.37.
|
|
43
|
+
"@ossy/platform": "^1.36.2",
|
|
44
|
+
"@ossy/router": "^1.37.2",
|
|
45
|
+
"@ossy/router-react": "^1.37.2",
|
|
46
|
+
"@ossy/sdk": "^1.37.2",
|
|
47
|
+
"@ossy/sdk-react": "^1.37.2",
|
|
48
|
+
"@ossy/themes": "^1.37.2",
|
|
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": "
|
|
82
|
+
"gitHead": "723d9d35aba3cd43700ecd6e72a7e7de96acff65"
|
|
83
83
|
}
|
package/runtime/page-runtime.js
CHANGED
|
@@ -26,8 +26,10 @@ export async function loadComponents (entries = []) {
|
|
|
26
26
|
return map
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function buildTree ({ Component, metadata, props }) {
|
|
29
|
+
function buildTree ({ Component, Layout, metadata, props }) {
|
|
30
30
|
const lang = props.htmlLang || props.defaultLanguage || 'en'
|
|
31
|
+
const pageEl = createElement(Component, props)
|
|
32
|
+
const contentEl = Layout ? createElement(Layout, props, pageEl) : pageEl
|
|
31
33
|
return createElement(
|
|
32
34
|
'html',
|
|
33
35
|
{ lang },
|
|
@@ -37,7 +39,7 @@ function buildTree ({ Component, metadata, props }) {
|
|
|
37
39
|
createElement('meta', { charSet: 'utf-8' }),
|
|
38
40
|
createElement('title', null, metadata.title || props.documentTitle || ''),
|
|
39
41
|
),
|
|
40
|
-
createElement(App, props,
|
|
42
|
+
createElement(App, props, contentEl),
|
|
41
43
|
)
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -75,9 +77,9 @@ export function createPageEntry (pageModule, options = {}) {
|
|
|
75
77
|
import('node:stream'),
|
|
76
78
|
])
|
|
77
79
|
|
|
78
|
-
const { componentEntries = [], ...pageProps } = props
|
|
80
|
+
const { Layout = null, componentEntries = [], ...pageProps } = props
|
|
79
81
|
const components = await loadComponents(componentEntries)
|
|
80
|
-
const tree = buildTree({ Component, metadata, props: { ...pageProps, components } })
|
|
82
|
+
const tree = buildTree({ Component, Layout, metadata, props: { ...pageProps, components } })
|
|
81
83
|
const bootstrapUrl = toBootstrapUrl(entryUrl)
|
|
82
84
|
const bootstrapModules = bootstrapUrl ? [bootstrapUrl] : []
|
|
83
85
|
const bootstrapScriptContent =
|
|
@@ -109,12 +111,12 @@ export function createPageEntry (pageModule, options = {}) {
|
|
|
109
111
|
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
110
112
|
hydrated = true
|
|
111
113
|
const props = window.__OSSY__ || {}
|
|
112
|
-
const { componentEntries = [], ...pageProps } = props
|
|
114
|
+
const { Layout = null, componentEntries = [], ...pageProps } = props
|
|
113
115
|
Promise.all([
|
|
114
116
|
import('react-dom/client'),
|
|
115
117
|
loadComponents(componentEntries),
|
|
116
118
|
]).then(([{ hydrateRoot }, components]) => {
|
|
117
|
-
hydrateRoot(document, buildTree({ Component, metadata, props: { ...pageProps, components } }))
|
|
119
|
+
hydrateRoot(document, buildTree({ Component, Layout, metadata, props: { ...pageProps, components } }))
|
|
118
120
|
})
|
|
119
121
|
}
|
|
120
122
|
|