@ossy/app 1.40.3 → 3.0.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/README.md +6 -0
- package/cli/get-platform-files.task.js +4 -6
- package/cli/manifest-plugin.js +111 -36
- package/package.json +23 -14
- package/runtime/merge-shell-slots.js +148 -0
- package/runtime/page-runtime.js +32 -16
- package/runtime/resolve-app-slots.js +48 -11
- package/src/en.translations.json +8 -0
- package/src/manifest/build-actions-schema.js +1 -36
- package/src/manifest/build-capabilities.js +1 -190
- package/src/manifest/build-manifest-summary.js +1 -121
- package/src/manifest/discover-package-definitions.js +1 -100
- package/src/manifest/resolve-page-layout.js +51 -0
- package/src/manifest/serialize-package-definition.js +1 -30
- package/src/shell/App.jsx +13 -7
- package/src/shell/AppSettings.jsx +6 -0
- package/src/shell/DevPagesPanel.jsx +1 -1
- package/src/shell/HeaderAuthActions.jsx +49 -0
- package/src/shell/LanguageList.jsx +83 -0
- package/src/shell/LanguageSelect.jsx +73 -0
- package/src/shell/ThemeEditor.jsx +2 -2
- package/src/shell/ThemeSelect.jsx +101 -0
- package/src/shell/buildSidebarNav.js +86 -0
- package/src/shell/index.js +7 -0
- package/src/shell/languageCode.js +31 -0
- package/src/shell/resolvePackageHomePageId.js +14 -0
- package/src/shell/useCompactShellLayout.js +24 -0
- package/src/shell-registry/blank.layout.jsx +8 -0
- package/src/shell-registry/default.layout.jsx +101 -0
- package/src/shell-registry/footer-default.component.jsx +39 -0
- package/src/shell-registry/head-default.component.jsx +17 -0
- package/src/shell-registry/header-default.component.jsx +50 -0
- package/src/shell-registry/logo-logomark.component.jsx +8 -0
- package/src/shell-registry/logo-logotype.component.jsx +15 -0
- package/src/shell-registry/minimal.layout.jsx +58 -0
- package/src/shell-registry/sidebar-default.component.jsx +281 -0
- package/src/sv.translations.json +8 -0
- package/src/manifest/action-id-to-tool-name.js +0 -29
- package/src/manifest/build-action-input-schema.js +0 -220
- package/src/manifest/template-to-json-schema.js +0 -103
package/README.md
CHANGED
|
@@ -70,6 +70,12 @@ export default {
|
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Optional flags:
|
|
74
|
+
|
|
75
|
+
| Key | Default | Purpose |
|
|
76
|
+
|-----|---------|---------|
|
|
77
|
+
| `enablePushInvalidation` | `false` | App-wide SSE cache invalidation. Prefer `<PushInvalidationSubscriber />` on specific pages instead — see [@ossy/sdk-react README](../sdk-react/README.md#push-invalidation). |
|
|
78
|
+
|
|
73
79
|
Config is loaded at build time and merged with request-time settings (e.g. user theme preference from cookies).
|
|
74
80
|
|
|
75
81
|
## API routes
|
|
@@ -40,7 +40,7 @@ export const TRANSLATIONS_FILE_PATTERN = /\.translations\.json$/
|
|
|
40
40
|
* @property {PlatformEntry[]} forms Discovered `*.form.{js,mjs,cjs}` entries.
|
|
41
41
|
* @property {PlatformEntry[]} e2es Discovered `*.e2e.{js,mjs,cjs}` entries (installed packages only).
|
|
42
42
|
* @property {PlatformEntry[]} flows Discovered `*.flow.{js,mjs,cjs}` entries (installed packages only).
|
|
43
|
-
* @property {PlatformEntry[]} layouts Discovered `*.layout.{jsx,tsx,...}` entries (app
|
|
43
|
+
* @property {PlatformEntry[]} layouts Discovered `*.layout.{jsx,tsx,...}` entries (app and installed packages).
|
|
44
44
|
*/
|
|
45
45
|
export function discoverFilesByPattern (srcDir, filePattern) {
|
|
46
46
|
const dir = path.resolve(srcDir)
|
|
@@ -183,8 +183,7 @@ export function discoverInstalledPackageEntries (projectRoot) {
|
|
|
183
183
|
...discoverFilesByPattern(packageSrcDir, FORM_FILE_PATTERN),
|
|
184
184
|
...discoverFilesByPattern(packageSrcDir, E2E_FILE_PATTERN),
|
|
185
185
|
...discoverFilesByPattern(packageSrcDir, FLOW_FILE_PATTERN),
|
|
186
|
-
|
|
187
|
-
// primitive only. Feature packages must never define layouts.
|
|
186
|
+
...discoverFilesByPattern(packageSrcDir, LAYOUT_FILE_PATTERN),
|
|
188
187
|
]
|
|
189
188
|
for (const sourcePath of files) {
|
|
190
189
|
const stat = fs.statSync(sourcePath)
|
|
@@ -237,9 +236,8 @@ export default async function getPlatformFiles (srcDir) {
|
|
|
237
236
|
// from the local `src/` — to avoid pulling in per-project duplicates of the
|
|
238
237
|
// same aggregate class or test suite that is canonically owned by the feature
|
|
239
238
|
// package.
|
|
240
|
-
// Layouts are
|
|
241
|
-
// app
|
|
242
|
-
// content (pages, actions, tasks, etc.) and must never define layouts.
|
|
239
|
+
// Layouts are discovered from the app's `src/` and from installed packages
|
|
240
|
+
// (`@ossy/app` ships default layouts via `ossy.src`).
|
|
243
241
|
const localFiles = [
|
|
244
242
|
...discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN),
|
|
245
243
|
...discoverFilesByPattern(srcDir, API_FILE_PATTERN),
|
package/cli/manifest-plugin.js
CHANGED
|
@@ -3,9 +3,9 @@ import path from 'node:path'
|
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
|
|
5
5
|
import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.js'
|
|
6
|
-
import { discoverPackageDefinitions } from '
|
|
7
|
-
import { buildActionsSchema } from '
|
|
8
|
-
import { buildCapabilities } from '
|
|
6
|
+
import { discoverPackageDefinitions } from '@ossy/manifest/discover-package-definitions'
|
|
7
|
+
import { buildActionsSchema } from '@ossy/manifest/build-actions-schema'
|
|
8
|
+
import { buildCapabilities } from '@ossy/manifest/build-capabilities'
|
|
9
9
|
import { extractTaskCatalogEntry, buildTaskCatalogEdges } from '../src/manifest/extract-task-catalog.js'
|
|
10
10
|
import {
|
|
11
11
|
Schema,
|
|
@@ -14,6 +14,11 @@ import {
|
|
|
14
14
|
CAPABILITY_SCHEMA_ID,
|
|
15
15
|
} from '@ossy/schema'
|
|
16
16
|
import { isCanonicalSchemaId } from '@ossy/schema'
|
|
17
|
+
import {
|
|
18
|
+
resolvePageLayoutId,
|
|
19
|
+
resolvePageShellSlots,
|
|
20
|
+
isCanonicalLayoutId,
|
|
21
|
+
} from '../src/manifest/resolve-page-layout.js'
|
|
17
22
|
|
|
18
23
|
/**
|
|
19
24
|
* @typedef {import('./get-platform-files.task.js').PlatformEntry} PlatformEntry
|
|
@@ -86,7 +91,7 @@ import { isCanonicalSchemaId } from '@ossy/schema'
|
|
|
86
91
|
* @property {string} entry URL the platform serves the form bundle from.
|
|
87
92
|
*
|
|
88
93
|
* @typedef {object} LayoutManifestEntry
|
|
89
|
-
* @property {string} id
|
|
94
|
+
* @property {string} id Canonical layout id (e.g. `'@ossy/app/layout/default'`).
|
|
90
95
|
* @property {string} entry URL the platform serves the layout bundle from.
|
|
91
96
|
* @property {Record<string, string>} [slots]
|
|
92
97
|
* App-controlled slot map (`slotName` → component `metadata.id`), from `export const slots` in `*.layout.jsx`.
|
|
@@ -164,6 +169,17 @@ function entryPackage (entryInfo, appPackageName) {
|
|
|
164
169
|
return entryInfo.packageName || appPackageName
|
|
165
170
|
}
|
|
166
171
|
|
|
172
|
+
/** Repo-relative path for Storybook / dev tooling (Vite source imports). */
|
|
173
|
+
function manifestSourcePath (entryInfo, projectRoot) {
|
|
174
|
+
if (!entryInfo?.sourcePath || !projectRoot) return undefined
|
|
175
|
+
return path.relative(projectRoot, entryInfo.sourcePath).split(path.sep).join('/')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function withSourcePath (obj, entryInfo, projectRoot) {
|
|
179
|
+
const sourcePath = manifestSourcePath(entryInfo, projectRoot)
|
|
180
|
+
return sourcePath ? { ...obj, sourcePath } : obj
|
|
181
|
+
}
|
|
182
|
+
|
|
167
183
|
export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configValue, appPackageName = '@ossy/app', manifestPath, projectRoot }) {
|
|
168
184
|
return {
|
|
169
185
|
name: 'ossy-manifest',
|
|
@@ -205,6 +221,8 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
205
221
|
/** @type {LayoutManifestEntry[]} */
|
|
206
222
|
const layouts = []
|
|
207
223
|
const seenLayoutIds = new Set()
|
|
224
|
+
/** @type {Map<string, { rawMeta: object, packageName: string }>} */
|
|
225
|
+
const pageMetaById = new Map()
|
|
208
226
|
/** @type {Array<{ mod: object, entryInfo: import('./get-platform-files.task.js').PlatformEntry, url: string, pkg: string }>} */
|
|
209
227
|
const taskPending = []
|
|
210
228
|
for (const fileName of Object.keys(bundle)) {
|
|
@@ -256,7 +274,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
256
274
|
this.error(`[@ossy/app][build] Duplicate integration id "${integrationId}"`)
|
|
257
275
|
}
|
|
258
276
|
seenIntegrationIds.add(integrationId)
|
|
259
|
-
integrations.push({ id: integrationId, entry: url, credentials, package: entryPackage(entryInfo, appPackageName) })
|
|
277
|
+
integrations.push(withSourcePath({ id: integrationId, entry: url, credentials, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
260
278
|
continue
|
|
261
279
|
}
|
|
262
280
|
|
|
@@ -279,7 +297,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
279
297
|
this.error(`[@ossy/app][build] Duplicate startup id "${startupId}"`)
|
|
280
298
|
}
|
|
281
299
|
seenStartupIds.add(startupId)
|
|
282
|
-
startups.push({ id: startupId, entry: url, package: entryPackage(entryInfo, appPackageName) })
|
|
300
|
+
startups.push(withSourcePath({ id: startupId, entry: url, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
283
301
|
continue
|
|
284
302
|
}
|
|
285
303
|
|
|
@@ -303,7 +321,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
303
321
|
this.error(`[@ossy/app][build] Duplicate email id "${emailId}"`)
|
|
304
322
|
}
|
|
305
323
|
seenEmailIds.add(emailId)
|
|
306
|
-
emails.push({ id: emailId, entry: url, package: entryPackage(entryInfo, appPackageName) })
|
|
324
|
+
emails.push(withSourcePath({ id: emailId, entry: url, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
307
325
|
continue
|
|
308
326
|
}
|
|
309
327
|
|
|
@@ -330,7 +348,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
330
348
|
entryInfo.sourcePath,
|
|
331
349
|
)
|
|
332
350
|
seenActionIds.add(actionId)
|
|
333
|
-
actions.push({ id: actionId, entry: url, access, package: entryPackage(entryInfo, appPackageName) })
|
|
351
|
+
actions.push(withSourcePath({ id: actionId, entry: url, access, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
334
352
|
continue
|
|
335
353
|
}
|
|
336
354
|
|
|
@@ -352,12 +370,12 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
352
370
|
this.error(`[@ossy/app][build] Duplicate form id "${formId}"`)
|
|
353
371
|
}
|
|
354
372
|
seenFormIds.add(formId)
|
|
355
|
-
forms.push({
|
|
373
|
+
forms.push(withSourcePath({
|
|
356
374
|
id: formId,
|
|
357
375
|
schemaId,
|
|
358
376
|
entry: url,
|
|
359
377
|
package: entryPackage(entryInfo, appPackageName),
|
|
360
|
-
})
|
|
378
|
+
}, entryInfo, projectRoot))
|
|
361
379
|
continue
|
|
362
380
|
}
|
|
363
381
|
|
|
@@ -381,7 +399,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
381
399
|
this.error(`[@ossy/app][build] Duplicate aggregate id "${aggregateId}"`)
|
|
382
400
|
}
|
|
383
401
|
seenAggregateIds.add(aggregateId)
|
|
384
|
-
aggregates.push({ id: aggregateId, entry: url, package: entryPackage(entryInfo, appPackageName) })
|
|
402
|
+
aggregates.push(withSourcePath({ id: aggregateId, entry: url, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
385
403
|
continue
|
|
386
404
|
}
|
|
387
405
|
|
|
@@ -409,13 +427,13 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
409
427
|
this.error(`[@ossy/app][build] Duplicate e2e id "${e2eId}"`)
|
|
410
428
|
}
|
|
411
429
|
seenE2eIds.add(e2eId)
|
|
412
|
-
e2es.push({
|
|
430
|
+
e2es.push(withSourcePath({
|
|
413
431
|
id: e2eId,
|
|
414
432
|
feature: typeof feature === 'string' ? feature : '',
|
|
415
433
|
requires: Array.isArray(requires) ? requires : [],
|
|
416
434
|
entry: url,
|
|
417
435
|
package: entryPackage(entryInfo, appPackageName),
|
|
418
|
-
})
|
|
436
|
+
}, entryInfo, projectRoot))
|
|
419
437
|
continue
|
|
420
438
|
}
|
|
421
439
|
|
|
@@ -439,26 +457,36 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
439
457
|
this.error(`[@ossy/app][build] Duplicate flow id "${flowId}"`)
|
|
440
458
|
}
|
|
441
459
|
seenFlowIds.add(flowId)
|
|
442
|
-
flows.push({
|
|
460
|
+
flows.push(withSourcePath({
|
|
443
461
|
id: flowId,
|
|
444
462
|
feature: typeof feature === 'string' ? feature : '',
|
|
445
463
|
requires: Array.isArray(requires) ? requires : [],
|
|
446
464
|
entry: url,
|
|
447
465
|
package: entryPackage(entryInfo, appPackageName),
|
|
448
|
-
})
|
|
466
|
+
}, entryInfo, projectRoot))
|
|
449
467
|
continue
|
|
450
468
|
}
|
|
451
469
|
|
|
452
|
-
// Layouts wrap
|
|
453
|
-
// export (the React component).
|
|
454
|
-
//
|
|
455
|
-
// `manifest.layouts` so the server can inject them at render time.
|
|
470
|
+
// Layouts wrap page renders. The bundle exports metadata.id, optional slots,
|
|
471
|
+
// and a default export (the React component). They accumulate into
|
|
472
|
+
// `manifest.layouts` so the server can inject them at render time per page.
|
|
456
473
|
if (entryInfo.kind === 'layout') {
|
|
457
|
-
const
|
|
474
|
+
const layoutMeta = mod?.metadata ?? {}
|
|
475
|
+
const layoutId =
|
|
476
|
+
typeof layoutMeta.id === 'string' && layoutMeta.id.trim()
|
|
477
|
+
? layoutMeta.id.trim()
|
|
478
|
+
: typeof mod?.id === 'string'
|
|
479
|
+
? mod.id.trim()
|
|
480
|
+
: ''
|
|
458
481
|
const component = mod && mod.default
|
|
459
|
-
if (
|
|
482
|
+
if (!layoutId) {
|
|
460
483
|
this.error(
|
|
461
|
-
`[@ossy/app][build] layout entry ${entryInfo.sourcePath} must export
|
|
484
|
+
`[@ossy/app][build] layout entry ${entryInfo.sourcePath} must export metadata.id (canonical layout id)`,
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
if (!isCanonicalLayoutId(layoutId)) {
|
|
488
|
+
this.error(
|
|
489
|
+
`[@ossy/app][build] layout entry ${entryInfo.sourcePath} id must match @{provider}/{feature}/layout/{concept} — got "${layoutId}"`,
|
|
462
490
|
)
|
|
463
491
|
}
|
|
464
492
|
if (typeof component !== 'function') {
|
|
@@ -496,12 +524,12 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
496
524
|
layoutSlots[sk] = cid
|
|
497
525
|
}
|
|
498
526
|
}
|
|
499
|
-
layouts.push({
|
|
527
|
+
layouts.push(withSourcePath({
|
|
500
528
|
id: layoutId,
|
|
501
529
|
entry: url,
|
|
502
530
|
package: entryPackage(entryInfo, appPackageName),
|
|
503
531
|
...(layoutSlots && Object.keys(layoutSlots).length > 0 ? { slots: layoutSlots } : {}),
|
|
504
|
-
})
|
|
532
|
+
}, entryInfo, projectRoot))
|
|
505
533
|
continue
|
|
506
534
|
}
|
|
507
535
|
|
|
@@ -526,7 +554,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
526
554
|
this.error(`[@ossy/app][build] Duplicate schema id "${schema.id}"`)
|
|
527
555
|
}
|
|
528
556
|
seenResourceIds.add(schema.id)
|
|
529
|
-
schemas.push({ ...schema, package: entryPackage(entryInfo, appPackageName) })
|
|
557
|
+
schemas.push(withSourcePath({ ...schema, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
530
558
|
continue
|
|
531
559
|
}
|
|
532
560
|
|
|
@@ -549,7 +577,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
549
577
|
seenIds.task.add(taskId)
|
|
550
578
|
const pkg = entryPackage(entryInfo, appPackageName)
|
|
551
579
|
taskPending.push({ mod, entryInfo, url, pkg })
|
|
552
|
-
entries.push({ type: 'task', id: taskId, entry: url, package: pkg })
|
|
580
|
+
entries.push(withSourcePath({ type: 'task', id: taskId, entry: url, package: pkg }, entryInfo, projectRoot))
|
|
553
581
|
continue
|
|
554
582
|
}
|
|
555
583
|
|
|
@@ -559,6 +587,14 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
559
587
|
this.error(`[@ossy/app][build] ${entryInfo.kind} entry missing metadata.id: ${entryInfo.sourcePath}`)
|
|
560
588
|
}
|
|
561
589
|
if (seenIds[entryInfo.kind].has(id)) {
|
|
590
|
+
// App-local components are discovered first. Allow the app to shadow
|
|
591
|
+
// the same component id from an installed package (e.g. brand logos).
|
|
592
|
+
if (entryInfo.kind === 'component' && entryInfo.packageName) {
|
|
593
|
+
console.warn(
|
|
594
|
+
`[@ossy/app][build] Skipping package component "${id}" from ${entryInfo.packageName} — app already provides it`,
|
|
595
|
+
)
|
|
596
|
+
continue
|
|
597
|
+
}
|
|
562
598
|
this.error(`[@ossy/app][build] Duplicate ${entryInfo.kind} id "${id}"`)
|
|
563
599
|
}
|
|
564
600
|
seenIds[entryInfo.kind].add(id)
|
|
@@ -570,7 +606,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
570
606
|
'Assign components to app slots in `*.layout.jsx` via `export const slots`.',
|
|
571
607
|
)
|
|
572
608
|
}
|
|
573
|
-
components.push({ id, entry: url, package: entryPackage(entryInfo, appPackageName) })
|
|
609
|
+
components.push(withSourcePath({ id, entry: url, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
574
610
|
continue
|
|
575
611
|
}
|
|
576
612
|
|
|
@@ -589,13 +625,17 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
589
625
|
),
|
|
590
626
|
entryInfo.sourcePath,
|
|
591
627
|
)
|
|
592
|
-
entries.push({
|
|
628
|
+
entries.push(withSourcePath({
|
|
593
629
|
type: 'page',
|
|
594
630
|
id,
|
|
595
631
|
path: pagePath,
|
|
596
632
|
title: rawMeta.title,
|
|
597
633
|
entry: url,
|
|
598
634
|
package: entryPackage(entryInfo, appPackageName),
|
|
635
|
+
}, entryInfo, projectRoot))
|
|
636
|
+
pageMetaById.set(id, {
|
|
637
|
+
rawMeta,
|
|
638
|
+
packageName: entryPackage(entryInfo, appPackageName),
|
|
599
639
|
})
|
|
600
640
|
} else if (entryInfo.kind === 'api') {
|
|
601
641
|
// APIs have no sensible default route (unlike pages where we can
|
|
@@ -626,7 +666,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
626
666
|
...(Array.isArray(rawMeta.query) && rawMeta.query.length ? { query: rawMeta.query } : {}),
|
|
627
667
|
}
|
|
628
668
|
}
|
|
629
|
-
entries.push(apiEntry)
|
|
669
|
+
entries.push(withSourcePath(apiEntry, entryInfo, projectRoot))
|
|
630
670
|
}
|
|
631
671
|
}
|
|
632
672
|
|
|
@@ -677,18 +717,53 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
677
717
|
|
|
678
718
|
const taskGraphEdges = buildTaskCatalogEdges(taskCatalog)
|
|
679
719
|
|
|
680
|
-
if (layouts.length > 1) {
|
|
681
|
-
this.error(
|
|
682
|
-
`[@ossy/app][build] Only one *.layout.jsx per app is allowed (found ${layouts.length}). ` +
|
|
683
|
-
'Use conditional rendering inside the app layout for route-specific chrome.',
|
|
684
|
-
)
|
|
685
|
-
}
|
|
686
|
-
|
|
687
720
|
/** @type {Manifest} */
|
|
688
721
|
const definitions = projectRoot
|
|
689
722
|
? await discoverPackageDefinitions(projectRoot)
|
|
690
723
|
: {}
|
|
691
724
|
|
|
725
|
+
const layoutIdSet = new Set(layouts.map((layout) => layout.id))
|
|
726
|
+
const componentIdSet = new Set(components.map((component) => component.id))
|
|
727
|
+
|
|
728
|
+
for (const entry of entries) {
|
|
729
|
+
if (entry.type !== 'page') continue
|
|
730
|
+
const meta = pageMetaById.get(entry.id)
|
|
731
|
+
const layoutId = resolvePageLayoutId({
|
|
732
|
+
pageMetadata: meta?.rawMeta ?? {},
|
|
733
|
+
appConfig: configValue ?? {},
|
|
734
|
+
})
|
|
735
|
+
if (!layoutIdSet.has(layoutId)) {
|
|
736
|
+
this.error(
|
|
737
|
+
`[@ossy/app][build] page "${entry.id}" references layout "${layoutId}" which is not in manifest.layouts`,
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
if (!isCanonicalLayoutId(layoutId)) {
|
|
741
|
+
this.error(
|
|
742
|
+
`[@ossy/app][build] page "${entry.id}" layout must match @{provider}/{feature}/layout/{concept} — got "${layoutId}"`,
|
|
743
|
+
)
|
|
744
|
+
}
|
|
745
|
+
entry.layout = layoutId
|
|
746
|
+
const layoutDef = layouts.find((layout) => layout.id === layoutId)
|
|
747
|
+
const mergedSlots = resolvePageShellSlots({
|
|
748
|
+
layoutSlots: layoutDef?.slots,
|
|
749
|
+
appSlots: configValue?.slots,
|
|
750
|
+
pageSlots: meta?.rawMeta?.slots,
|
|
751
|
+
})
|
|
752
|
+
if (Object.keys(mergedSlots).length > 0) {
|
|
753
|
+
entry.slots = mergedSlots
|
|
754
|
+
}
|
|
755
|
+
for (const [slotKey, spec] of Object.entries(mergedSlots)) {
|
|
756
|
+
if (spec?.view === null) continue
|
|
757
|
+
const componentId = typeof spec?.view === 'string' ? spec.view.trim() : ''
|
|
758
|
+
if (!componentId) continue
|
|
759
|
+
if (!componentIdSet.has(componentId)) {
|
|
760
|
+
this.error(
|
|
761
|
+
`[@ossy/app][build] page "${entry.id}" slot "${slotKey}" references unknown component id "${componentId}"`,
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
692
767
|
const manifest = {
|
|
693
768
|
entries,
|
|
694
769
|
components,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"source": "./src/index.js",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
"./manifest/build-actions-schema": "./src/manifest/build-actions-schema.js",
|
|
17
17
|
"./manifest/build-capabilities": "./src/manifest/build-capabilities.js",
|
|
18
18
|
"./manifest/discover-package-definitions": "./src/manifest/discover-package-definitions.js",
|
|
19
|
-
"./manifest/serialize-package-definition": "./src/manifest/serialize-package-definition.js"
|
|
19
|
+
"./manifest/serialize-package-definition": "./src/manifest/serialize-package-definition.js",
|
|
20
|
+
"./manifest/resolve-page-layout": "./src/manifest/resolve-page-layout.js",
|
|
21
|
+
"./runtime/merge-shell-slots": "./runtime/merge-shell-slots.js"
|
|
22
|
+
},
|
|
23
|
+
"ossy": {
|
|
24
|
+
"src": "./src/shell-registry"
|
|
20
25
|
},
|
|
21
26
|
"bin": {
|
|
22
27
|
"app": "./cli/index.js"
|
|
@@ -44,17 +49,21 @@
|
|
|
44
49
|
"@babel/eslint-parser": "^7.15.8",
|
|
45
50
|
"@babel/preset-react": "^7.26.3",
|
|
46
51
|
"@babel/register": "^7.25.9",
|
|
47
|
-
"@ossy/
|
|
48
|
-
"@ossy/
|
|
49
|
-
"@ossy/
|
|
50
|
-
"@ossy/
|
|
51
|
-
"@ossy/
|
|
52
|
-
"@ossy/
|
|
53
|
-
"@ossy/
|
|
54
|
-
"@ossy/
|
|
55
|
-
"@ossy/
|
|
56
|
-
"@ossy/
|
|
57
|
-
"@ossy/
|
|
52
|
+
"@ossy/authentication": "^3.0.2",
|
|
53
|
+
"@ossy/design-system": "^3.0.2",
|
|
54
|
+
"@ossy/locale": "^3.0.2",
|
|
55
|
+
"@ossy/manifest": "^3.0.2",
|
|
56
|
+
"@ossy/package-catalog": "^3.0.2",
|
|
57
|
+
"@ossy/pages": "^3.0.2",
|
|
58
|
+
"@ossy/platform": "^3.0.2",
|
|
59
|
+
"@ossy/resources": "^3.0.2",
|
|
60
|
+
"@ossy/router": "^3.0.2",
|
|
61
|
+
"@ossy/router-react": "^3.0.2",
|
|
62
|
+
"@ossy/schema": "^3.0.2",
|
|
63
|
+
"@ossy/sdk": "^3.0.2",
|
|
64
|
+
"@ossy/sdk-react": "^3.0.2",
|
|
65
|
+
"@ossy/themes": "^3.0.2",
|
|
66
|
+
"@ossy/workspaces": "^3.0.2",
|
|
58
67
|
"@rollup/plugin-alias": "^6.0.0",
|
|
59
68
|
"@rollup/plugin-babel": "^7.0.0",
|
|
60
69
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
@@ -88,5 +97,5 @@
|
|
|
88
97
|
"README.md",
|
|
89
98
|
"tsconfig.json"
|
|
90
99
|
],
|
|
91
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "f0a8af3c370bbaffe4f7388c14f9345a5c6c56e5"
|
|
92
101
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { CONTENT_SLOT_NAME, normalizeAppSlotName } from './resolve-app-slots.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{ view?: string | null, props: Record<string, unknown> }} ShellSlotSpec
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Coerce a manifest/runtime slot value to {@link ShellSlotSpec}.
|
|
9
|
+
*
|
|
10
|
+
* @param {string | null | ShellSlotSpec | undefined} value
|
|
11
|
+
* @returns {ShellSlotSpec | undefined}
|
|
12
|
+
*/
|
|
13
|
+
export function coerceShellSlotSpec (value) {
|
|
14
|
+
if (value === null) return { view: null, props: {} }
|
|
15
|
+
if (typeof value === 'string') {
|
|
16
|
+
const trimmed = value.trim()
|
|
17
|
+
return trimmed ? { view: trimmed, props: {} } : undefined
|
|
18
|
+
}
|
|
19
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
20
|
+
const { view, props: nestedProps, ...rest } = value
|
|
21
|
+
const propBag = {
|
|
22
|
+
...(nestedProps && typeof nestedProps === 'object' && !Array.isArray(nestedProps) ? nestedProps : {}),
|
|
23
|
+
...rest,
|
|
24
|
+
}
|
|
25
|
+
/** @type {ShellSlotSpec} */
|
|
26
|
+
const spec = { props: propBag }
|
|
27
|
+
if (view === null) spec.view = null
|
|
28
|
+
else if (typeof view === 'string') {
|
|
29
|
+
const trimmed = view.trim()
|
|
30
|
+
if (trimmed) spec.view = trimmed
|
|
31
|
+
}
|
|
32
|
+
return spec
|
|
33
|
+
}
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Apply one layer onto the accumulated slot spec (layout → app → page).
|
|
39
|
+
*
|
|
40
|
+
* - `null` / `{ view: null }` → unset
|
|
41
|
+
* - string → set view, keep inherited props
|
|
42
|
+
* - `{ …props }` → shallow-merge props, keep inherited view
|
|
43
|
+
* - `{ view, …props }` → set view + merge props
|
|
44
|
+
* - `{}` → no-op
|
|
45
|
+
*
|
|
46
|
+
* @param {ShellSlotSpec | undefined} current
|
|
47
|
+
* @param {string | null | Record<string, unknown> | undefined} value
|
|
48
|
+
* @returns {ShellSlotSpec | undefined}
|
|
49
|
+
*/
|
|
50
|
+
export function applyShellSlotLayer (current, value) {
|
|
51
|
+
if (value === null) {
|
|
52
|
+
return { view: null, props: {} }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
const trimmed = value.trim()
|
|
57
|
+
if (!trimmed) return current
|
|
58
|
+
return {
|
|
59
|
+
view: trimmed,
|
|
60
|
+
props: current?.props ? { ...current.props } : {},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
65
|
+
if (Object.keys(value).length === 0) {
|
|
66
|
+
return current
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { view, props: nestedProps, ...rest } = value
|
|
70
|
+
if (view === null) {
|
|
71
|
+
return { view: null, props: {} }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const restProps = {
|
|
75
|
+
...(nestedProps && typeof nestedProps === 'object' && !Array.isArray(nestedProps) ? nestedProps : {}),
|
|
76
|
+
...rest,
|
|
77
|
+
}
|
|
78
|
+
const nextProps = Object.keys(restProps).length > 0
|
|
79
|
+
? { ...(current?.props ?? {}), ...restProps }
|
|
80
|
+
: { ...(current?.props ?? {}) }
|
|
81
|
+
|
|
82
|
+
/** @type {ShellSlotSpec} */
|
|
83
|
+
const next = { props: nextProps }
|
|
84
|
+
if (view !== undefined) {
|
|
85
|
+
if (typeof view === 'string') {
|
|
86
|
+
const trimmed = view.trim()
|
|
87
|
+
if (trimmed) next.view = trimmed
|
|
88
|
+
}
|
|
89
|
+
} else if (current?.view !== undefined) {
|
|
90
|
+
next.view = current.view
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return next
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return current
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {ShellSlotSpec | string | null | undefined} spec
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
export function shellSlotUnset (spec) {
|
|
104
|
+
const normalized = typeof spec === 'string' || spec === null ? coerceShellSlotSpec(spec) : spec
|
|
105
|
+
return normalized?.view === null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {ShellSlotSpec | string | null | undefined} spec
|
|
110
|
+
* @returns {string | undefined}
|
|
111
|
+
*/
|
|
112
|
+
export function shellSlotViewId (spec) {
|
|
113
|
+
const normalized = typeof spec === 'string' || spec === null ? coerceShellSlotSpec(spec) : spec
|
|
114
|
+
if (!normalized || normalized.view === null) return undefined
|
|
115
|
+
return typeof normalized.view === 'string' ? normalized.view : undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Merge shell slot maps per ADR 0012: layout defaults → app config → page metadata.
|
|
120
|
+
* Page layer wins for both view (when set) and props.
|
|
121
|
+
*
|
|
122
|
+
* @param {Record<string, string | null | Record<string, unknown>> | null | undefined} layoutSlots
|
|
123
|
+
* @param {Record<string, string | null | Record<string, unknown>> | null | undefined} appSlots
|
|
124
|
+
* @param {Record<string, string | null | Record<string, unknown>> | null | undefined} pageSlots
|
|
125
|
+
* @returns {Record<string, ShellSlotSpec>}
|
|
126
|
+
*/
|
|
127
|
+
export function mergeShellSlots (layoutSlots, appSlots, pageSlots) {
|
|
128
|
+
/** @type {Record<string, ShellSlotSpec>} */
|
|
129
|
+
const merged = {}
|
|
130
|
+
|
|
131
|
+
const apply = (map) => {
|
|
132
|
+
if (!map || typeof map !== 'object' || Array.isArray(map)) return
|
|
133
|
+
Object.entries(map).forEach(([rawKey, value]) => {
|
|
134
|
+
const key = normalizeAppSlotName(rawKey)
|
|
135
|
+
if (!key || key === CONTENT_SLOT_NAME) return
|
|
136
|
+
const next = applyShellSlotLayer(merged[key], value)
|
|
137
|
+
if (next !== undefined) {
|
|
138
|
+
merged[key] = next
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
apply(layoutSlots)
|
|
144
|
+
apply(appSlots)
|
|
145
|
+
apply(pageSlots)
|
|
146
|
+
|
|
147
|
+
return merged
|
|
148
|
+
}
|