@ossy/app 1.40.2 → 3.0.1
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 +49 -12
- package/cli/build.task.js +49 -10
- package/cli/get-platform-files.task.js +21 -15
- package/cli/manifest-plugin.js +340 -48
- package/package.json +25 -13
- package/runtime/merge-shell-slots.js +148 -0
- package/runtime/page-runtime.js +36 -18
- package/runtime/resolve-app-slots.js +126 -0
- package/src/en.translations.json +8 -0
- package/src/manifest/action-id-to-tool-name.js +29 -0
- package/src/manifest/build-action-input-schema.js +220 -0
- package/src/manifest/build-actions-schema.js +36 -0
- package/src/manifest/build-capabilities.js +190 -0
- package/src/manifest/build-manifest-summary.js +11 -5
- package/src/manifest/extract-task-catalog.js +1 -0
- package/src/manifest/resolve-page-layout.js +51 -0
- package/src/manifest/serialize-package-definition.js +2 -3
- package/src/manifest/template-to-json-schema.js +103 -0
- package/src/shell/App.jsx +18 -9
- package/src/shell/AppSettings.jsx +17 -0
- package/src/shell/DevPagesPanel.jsx +17 -13
- 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 +19 -34
- package/src/shell/ThemeSelect.jsx +101 -0
- package/src/shell/WorkspaceAppSettingsSync.jsx +39 -0
- package/src/shell/buildSidebarNav.js +113 -0
- package/src/shell/index.js +11 -0
- package/src/shell/languageCode.js +31 -0
- package/src/shell/patchUserAppSettings.js +10 -0
- package/src/shell/resolveEndpoints.js +43 -0
- package/src/shell/resolveWorkspaceServices.js +20 -0
- package/src/shell/themeEditorStyles.js +49 -0
- package/src/shell/useCompactShellLayout.js +24 -0
- package/src/shell/useShellWorkspace.js +40 -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/runtime/resolve-shell-slots.js +0 -112
package/cli/manifest-plugin.js
CHANGED
|
@@ -4,6 +4,21 @@ import { pathToFileURL } from 'node:url'
|
|
|
4
4
|
|
|
5
5
|
import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.js'
|
|
6
6
|
import { discoverPackageDefinitions } from '../src/manifest/discover-package-definitions.js'
|
|
7
|
+
import { buildActionsSchema } from '../src/manifest/build-actions-schema.js'
|
|
8
|
+
import { buildCapabilities } from '../src/manifest/build-capabilities.js'
|
|
9
|
+
import { extractTaskCatalogEntry, buildTaskCatalogEdges } from '../src/manifest/extract-task-catalog.js'
|
|
10
|
+
import {
|
|
11
|
+
Schema,
|
|
12
|
+
isActionId,
|
|
13
|
+
taskIdFromActionId,
|
|
14
|
+
CAPABILITY_SCHEMA_ID,
|
|
15
|
+
} from '@ossy/schema'
|
|
16
|
+
import { isCanonicalSchemaId } from '@ossy/schema'
|
|
17
|
+
import {
|
|
18
|
+
resolvePageLayoutId,
|
|
19
|
+
resolvePageShellSlots,
|
|
20
|
+
isCanonicalLayoutId,
|
|
21
|
+
} from '../src/manifest/resolve-page-layout.js'
|
|
7
22
|
|
|
8
23
|
/**
|
|
9
24
|
* @typedef {import('./get-platform-files.task.js').PlatformEntry} PlatformEntry
|
|
@@ -62,16 +77,21 @@ import { discoverPackageDefinitions } from '../src/manifest/discover-package-def
|
|
|
62
77
|
* @property {string} entry URL the platform serves the startup bundle from.
|
|
63
78
|
*
|
|
64
79
|
* @typedef {object} EmailManifestEntry
|
|
65
|
-
* @property {string} id Unique email id (e.g. `'authentication/verify-sign-in'`).
|
|
80
|
+
* @property {string} id Unique email id (e.g. `'@ossy/authentication/emails/verify-sign-in'`).
|
|
66
81
|
* @property {string} entry URL the platform serves the email bundle from.
|
|
67
82
|
*
|
|
68
83
|
* @typedef {object} ActionManifestEntry
|
|
69
|
-
* @property {string} id Unique action slug (e.g. `'authentication/request-sign-in'`).
|
|
84
|
+
* @property {string} id Unique action slug (e.g. `'@ossy/authentication/actions/request-sign-in'`).
|
|
70
85
|
* @property {string} entry URL the platform serves the action bundle from.
|
|
71
86
|
* @property {string} access Access level: `'public'` | `'authenticated'` | `'workspace'`.
|
|
72
87
|
*
|
|
88
|
+
* @typedef {object} FormManifestEntry
|
|
89
|
+
* @property {string} id Unique form slug (e.g. `'@ossy/authentication/form/sign-up'`).
|
|
90
|
+
* @property {string} schemaId Resource schema id (e.g. `'@ossy/authentication/sign-up'`).
|
|
91
|
+
* @property {string} entry URL the platform serves the form bundle from.
|
|
92
|
+
*
|
|
73
93
|
* @typedef {object} LayoutManifestEntry
|
|
74
|
-
* @property {string} id
|
|
94
|
+
* @property {string} id Canonical layout id (e.g. `'@ossy/app/layout/default'`).
|
|
75
95
|
* @property {string} entry URL the platform serves the layout bundle from.
|
|
76
96
|
* @property {Record<string, string>} [slots]
|
|
77
97
|
* App-controlled slot map (`slotName` → component `metadata.id`), from `export const slots` in `*.layout.jsx`.
|
|
@@ -82,16 +102,24 @@ import { discoverPackageDefinitions } from '../src/manifest/discover-package-def
|
|
|
82
102
|
* @property {string[]} requires Runtime dependencies needed (e.g. `['server', 'database']`).
|
|
83
103
|
* @property {string} entry URL the platform serves the e2e bundle from.
|
|
84
104
|
*
|
|
105
|
+
* @typedef {object} FlowManifestEntry
|
|
106
|
+
* @property {string} id Unique flow id (e.g. `'@ossy/authentication/actions/sign-up'`).
|
|
107
|
+
* @property {string} feature Grouping label (e.g. `'authentication'`).
|
|
108
|
+
* @property {string[]} requires Runtime dependencies needed (e.g. `['server', 'database']`).
|
|
109
|
+
* @property {string} entry URL the platform serves the flow bundle from.
|
|
110
|
+
*
|
|
85
111
|
* @typedef {object} Manifest
|
|
86
112
|
* @property {ManifestEntry[]} entries Flat, ordered list of every routable thing.
|
|
87
113
|
* @property {ComponentManifestEntry[]} components Discovered `*.component.*` entries, keyed by id.
|
|
88
|
-
* @property {object[]}
|
|
114
|
+
* @property {object[]} schemas Each `*.schema.js` file's default-exported template object.
|
|
89
115
|
* @property {AggregateManifestEntry[]} aggregates Discovered `*.aggregate.js` entries from installed packages.
|
|
90
116
|
* @property {IntegrationManifestEntry[]} integrations Discovered `*.integration.js` entries.
|
|
91
117
|
* @property {StartupManifestEntry[]} startups Discovered `*.startup.js` entries.
|
|
92
118
|
* @property {EmailManifestEntry[]} emails Discovered `*.email.{jsx,tsx}` entries.
|
|
93
119
|
* @property {ActionManifestEntry[]} actions Discovered `*.action.js` entries.
|
|
120
|
+
* @property {FormManifestEntry[]} forms Discovered `*.form.js` entries.
|
|
94
121
|
* @property {E2eManifestEntry[]} e2es Discovered `*.e2e.js` entries from installed packages.
|
|
122
|
+
* @property {FlowManifestEntry[]} flows Discovered `*.flow.js` entries from installed packages.
|
|
95
123
|
* @property {LayoutManifestEntry[]} layouts Discovered `*.layout.{jsx,tsx}` entries (includes app `slots` map).
|
|
96
124
|
* @property {object} config Inlined `src/config.js` default export.
|
|
97
125
|
* @property {Record<string, object>} [definitions] Package Definition metadata keyed by slug.
|
|
@@ -131,18 +159,37 @@ import { discoverPackageDefinitions } from '../src/manifest/discover-package-def
|
|
|
131
159
|
* @param {ManifestPluginOptions} options
|
|
132
160
|
* @returns {import('rollup').Plugin}
|
|
133
161
|
*/
|
|
162
|
+
function reportValidationError (plugin, result, sourcePath) {
|
|
163
|
+
if (result.ok) return
|
|
164
|
+
const message = result.errors?.[0]?.message ?? result.message ?? 'Validation failed'
|
|
165
|
+
plugin.error(`[@ossy/app][build] ${sourcePath}: ${message}`)
|
|
166
|
+
}
|
|
167
|
+
|
|
134
168
|
function entryPackage (entryInfo, appPackageName) {
|
|
135
169
|
return entryInfo.packageName || appPackageName
|
|
136
170
|
}
|
|
137
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
|
+
|
|
138
183
|
export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configValue, appPackageName = '@ossy/app', manifestPath, projectRoot }) {
|
|
139
184
|
return {
|
|
140
185
|
name: 'ossy-manifest',
|
|
141
186
|
async writeBundle (_, bundle) {
|
|
142
187
|
/** @type {ManifestEntry[]} */
|
|
143
188
|
const entries = []
|
|
189
|
+
/** @type {Record<string, { path: string, method?: string, query?: string[] }>} */
|
|
190
|
+
const actionRoutes = {}
|
|
144
191
|
/** @type {object[]} */
|
|
145
|
-
const
|
|
192
|
+
const schemas = []
|
|
146
193
|
/** @type {import('./get-platform-files.task.js').ComponentManifestEntry[]} */
|
|
147
194
|
const components = []
|
|
148
195
|
/** @type {AggregateManifestEntry[]} */
|
|
@@ -155,6 +202,8 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
155
202
|
const emails = []
|
|
156
203
|
/** @type {ActionManifestEntry[]} */
|
|
157
204
|
const actions = []
|
|
205
|
+
/** @type {FormManifestEntry[]} */
|
|
206
|
+
const forms = []
|
|
158
207
|
const seenIds = { page: new Set(), api: new Set(), task: new Set(), component: new Set() }
|
|
159
208
|
const seenResourceIds = new Set()
|
|
160
209
|
const seenAggregateIds = new Set()
|
|
@@ -162,12 +211,20 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
162
211
|
const seenStartupIds = new Set()
|
|
163
212
|
const seenEmailIds = new Set()
|
|
164
213
|
const seenActionIds = new Set()
|
|
214
|
+
const seenFormIds = new Set()
|
|
165
215
|
/** @type {E2eManifestEntry[]} */
|
|
166
216
|
const e2es = []
|
|
167
217
|
const seenE2eIds = new Set()
|
|
218
|
+
/** @type {FlowManifestEntry[]} */
|
|
219
|
+
const flows = []
|
|
220
|
+
const seenFlowIds = new Set()
|
|
168
221
|
/** @type {LayoutManifestEntry[]} */
|
|
169
222
|
const layouts = []
|
|
170
223
|
const seenLayoutIds = new Set()
|
|
224
|
+
/** @type {Map<string, { rawMeta: object, packageName: string }>} */
|
|
225
|
+
const pageMetaById = new Map()
|
|
226
|
+
/** @type {Array<{ mod: object, entryInfo: import('./get-platform-files.task.js').PlatformEntry, url: string, pkg: string }>} */
|
|
227
|
+
const taskPending = []
|
|
171
228
|
for (const fileName of Object.keys(bundle)) {
|
|
172
229
|
const chunk = bundle[fileName]
|
|
173
230
|
if (chunk.type !== 'chunk' || !chunk.isEntry) continue
|
|
@@ -217,7 +274,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
217
274
|
this.error(`[@ossy/app][build] Duplicate integration id "${integrationId}"`)
|
|
218
275
|
}
|
|
219
276
|
seenIntegrationIds.add(integrationId)
|
|
220
|
-
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))
|
|
221
278
|
continue
|
|
222
279
|
}
|
|
223
280
|
|
|
@@ -240,7 +297,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
240
297
|
this.error(`[@ossy/app][build] Duplicate startup id "${startupId}"`)
|
|
241
298
|
}
|
|
242
299
|
seenStartupIds.add(startupId)
|
|
243
|
-
startups.push({ id: startupId, entry: url, package: entryPackage(entryInfo, appPackageName) })
|
|
300
|
+
startups.push(withSourcePath({ id: startupId, entry: url, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
244
301
|
continue
|
|
245
302
|
}
|
|
246
303
|
|
|
@@ -264,12 +321,12 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
264
321
|
this.error(`[@ossy/app][build] Duplicate email id "${emailId}"`)
|
|
265
322
|
}
|
|
266
323
|
seenEmailIds.add(emailId)
|
|
267
|
-
emails.push({ id: emailId, entry: url, package: entryPackage(entryInfo, appPackageName) })
|
|
324
|
+
emails.push(withSourcePath({ id: emailId, entry: url, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
268
325
|
continue
|
|
269
326
|
}
|
|
270
327
|
|
|
271
|
-
// Actions are intent: `metadata` only (`{ id, access }`).
|
|
272
|
-
//
|
|
328
|
+
// Actions are intent: `metadata` only (`{ id, access }`). Primary implementation
|
|
329
|
+
// lives in `{feature}/tasks/{intent}` derived from the action id (ADR 0003).
|
|
273
330
|
if (entryInfo.kind === 'action') {
|
|
274
331
|
const actionId = mod?.metadata?.id
|
|
275
332
|
if (typeof actionId !== 'string' || actionId.trim() === '') {
|
|
@@ -281,8 +338,44 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
281
338
|
this.error(`[@ossy/app][build] Duplicate action id "${actionId}"`)
|
|
282
339
|
}
|
|
283
340
|
const access = mod.metadata?.access ?? 'authenticated'
|
|
341
|
+
reportValidationError(
|
|
342
|
+
this,
|
|
343
|
+
Schema.of({ schemas: [] }).validate(
|
|
344
|
+
CAPABILITY_SCHEMA_ID.ACTION_META,
|
|
345
|
+
{ id: actionId, ...(mod.metadata?.access != null ? { access: mod.metadata.access } : {}) },
|
|
346
|
+
{ sourcePath: entryInfo.sourcePath },
|
|
347
|
+
),
|
|
348
|
+
entryInfo.sourcePath,
|
|
349
|
+
)
|
|
284
350
|
seenActionIds.add(actionId)
|
|
285
|
-
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))
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Forms are intent metadata (`metadata` only) referencing a resource template.
|
|
356
|
+
if (entryInfo.kind === 'form') {
|
|
357
|
+
const formId = mod?.metadata?.id
|
|
358
|
+
const schemaId = mod?.metadata?.schemaId
|
|
359
|
+
if (typeof formId !== 'string' || formId.trim() === '') {
|
|
360
|
+
this.error(
|
|
361
|
+
`[@ossy/app][build] form entry ${entryInfo.sourcePath} must export a non-empty string "metadata.id"`,
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
if (typeof schemaId !== 'string' || schemaId.trim() === '') {
|
|
365
|
+
this.error(
|
|
366
|
+
`[@ossy/app][build] form entry ${entryInfo.sourcePath} must export a non-empty string "metadata.schemaId"`,
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
if (seenFormIds.has(formId)) {
|
|
370
|
+
this.error(`[@ossy/app][build] Duplicate form id "${formId}"`)
|
|
371
|
+
}
|
|
372
|
+
seenFormIds.add(formId)
|
|
373
|
+
forms.push(withSourcePath({
|
|
374
|
+
id: formId,
|
|
375
|
+
schemaId,
|
|
376
|
+
entry: url,
|
|
377
|
+
package: entryPackage(entryInfo, appPackageName),
|
|
378
|
+
}, entryInfo, projectRoot))
|
|
286
379
|
continue
|
|
287
380
|
}
|
|
288
381
|
|
|
@@ -306,7 +399,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
306
399
|
this.error(`[@ossy/app][build] Duplicate aggregate id "${aggregateId}"`)
|
|
307
400
|
}
|
|
308
401
|
seenAggregateIds.add(aggregateId)
|
|
309
|
-
aggregates.push({ id: aggregateId, entry: url, package: entryPackage(entryInfo, appPackageName) })
|
|
402
|
+
aggregates.push(withSourcePath({ id: aggregateId, entry: url, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
310
403
|
continue
|
|
311
404
|
}
|
|
312
405
|
|
|
@@ -334,26 +427,66 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
334
427
|
this.error(`[@ossy/app][build] Duplicate e2e id "${e2eId}"`)
|
|
335
428
|
}
|
|
336
429
|
seenE2eIds.add(e2eId)
|
|
337
|
-
e2es.push({
|
|
430
|
+
e2es.push(withSourcePath({
|
|
338
431
|
id: e2eId,
|
|
339
432
|
feature: typeof feature === 'string' ? feature : '',
|
|
340
433
|
requires: Array.isArray(requires) ? requires : [],
|
|
341
434
|
entry: url,
|
|
342
435
|
package: entryPackage(entryInfo, appPackageName),
|
|
343
|
-
})
|
|
436
|
+
}, entryInfo, projectRoot))
|
|
344
437
|
continue
|
|
345
438
|
}
|
|
346
439
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
440
|
+
if (entryInfo.kind === 'flow') {
|
|
441
|
+
const flowMeta = mod?.metadata ?? mod?.default?.metadata ?? {}
|
|
442
|
+
const flowId = flowMeta.id ?? mod?.default?.id
|
|
443
|
+
const feature = flowMeta.feature
|
|
444
|
+
const requires = flowMeta.requires
|
|
445
|
+
const flowBody = mod?.default
|
|
446
|
+
if (typeof flowId !== 'string' || flowId.trim() === '') {
|
|
447
|
+
this.error(
|
|
448
|
+
`[@ossy/app][build] flow entry ${entryInfo.sourcePath} must export metadata.id`,
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
if (!flowBody || typeof flowBody !== 'object' || !Array.isArray(flowBody.steps)) {
|
|
452
|
+
this.error(
|
|
453
|
+
`[@ossy/app][build] flow entry ${entryInfo.sourcePath} must default-export an object with steps[]`,
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
if (seenFlowIds.has(flowId)) {
|
|
457
|
+
this.error(`[@ossy/app][build] Duplicate flow id "${flowId}"`)
|
|
458
|
+
}
|
|
459
|
+
seenFlowIds.add(flowId)
|
|
460
|
+
flows.push(withSourcePath({
|
|
461
|
+
id: flowId,
|
|
462
|
+
feature: typeof feature === 'string' ? feature : '',
|
|
463
|
+
requires: Array.isArray(requires) ? requires : [],
|
|
464
|
+
entry: url,
|
|
465
|
+
package: entryPackage(entryInfo, appPackageName),
|
|
466
|
+
}, entryInfo, projectRoot))
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
|
|
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.
|
|
351
473
|
if (entryInfo.kind === 'layout') {
|
|
352
|
-
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
|
+
: ''
|
|
353
481
|
const component = mod && mod.default
|
|
354
|
-
if (
|
|
482
|
+
if (!layoutId) {
|
|
355
483
|
this.error(
|
|
356
|
-
`[@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}"`,
|
|
357
490
|
)
|
|
358
491
|
}
|
|
359
492
|
if (typeof component !== 'function') {
|
|
@@ -391,36 +524,60 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
391
524
|
layoutSlots[sk] = cid
|
|
392
525
|
}
|
|
393
526
|
}
|
|
394
|
-
layouts.push({
|
|
527
|
+
layouts.push(withSourcePath({
|
|
395
528
|
id: layoutId,
|
|
396
529
|
entry: url,
|
|
397
530
|
package: entryPackage(entryInfo, appPackageName),
|
|
398
531
|
...(layoutSlots && Object.keys(layoutSlots).length > 0 ? { slots: layoutSlots } : {}),
|
|
399
|
-
})
|
|
532
|
+
}, entryInfo, projectRoot))
|
|
533
|
+
continue
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (entryInfo.kind === 'schema') {
|
|
537
|
+
const schema = mod && mod.default
|
|
538
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
539
|
+
this.error(
|
|
540
|
+
`[@ossy/app][build] schema entry ${entryInfo.sourcePath} must default-export an object`,
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
if (typeof schema.id !== 'string' || schema.id.trim() === '') {
|
|
544
|
+
this.error(
|
|
545
|
+
`[@ossy/app][build] schema entry ${entryInfo.sourcePath} default export is missing a non-empty string "id"`,
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
if (!isCanonicalSchemaId(schema.id)) {
|
|
549
|
+
this.error(
|
|
550
|
+
`[@ossy/app][build] schema entry ${entryInfo.sourcePath} id must match @{provider}/{feature}/schema/{concept} — got "${schema.id}"`,
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
if (seenResourceIds.has(schema.id)) {
|
|
554
|
+
this.error(`[@ossy/app][build] Duplicate schema id "${schema.id}"`)
|
|
555
|
+
}
|
|
556
|
+
seenResourceIds.add(schema.id)
|
|
557
|
+
schemas.push(withSourcePath({ ...schema, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
400
558
|
continue
|
|
401
559
|
}
|
|
402
560
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (entryInfo.kind === 'resource') {
|
|
408
|
-
const template = mod && mod.default
|
|
409
|
-
if (!template || typeof template !== 'object' || Array.isArray(template)) {
|
|
561
|
+
if (entryInfo.kind === 'task') {
|
|
562
|
+
const taskMeta = mod?.metadata
|
|
563
|
+
const taskId = taskMeta?.id
|
|
564
|
+
if (typeof taskId !== 'string' || taskId.trim() === '') {
|
|
410
565
|
this.error(
|
|
411
|
-
`[@ossy/app][build]
|
|
566
|
+
`[@ossy/app][build] task entry ${entryInfo.sourcePath} must export a non-empty string "metadata.id"`,
|
|
412
567
|
)
|
|
413
568
|
}
|
|
414
|
-
if (
|
|
569
|
+
if (isActionId(taskId)) {
|
|
415
570
|
this.error(
|
|
416
|
-
`[@ossy/app][build]
|
|
571
|
+
`[@ossy/app][build] Task "${taskId}" uses /actions/ — use /tasks/ (e.g. "${taskIdFromActionId(taskId)}")`,
|
|
417
572
|
)
|
|
418
573
|
}
|
|
419
|
-
if (
|
|
420
|
-
this.error(`[@ossy/app][build] Duplicate
|
|
574
|
+
if (seenIds.task.has(taskId)) {
|
|
575
|
+
this.error(`[@ossy/app][build] Duplicate task id "${taskId}"`)
|
|
421
576
|
}
|
|
422
|
-
|
|
423
|
-
|
|
577
|
+
seenIds.task.add(taskId)
|
|
578
|
+
const pkg = entryPackage(entryInfo, appPackageName)
|
|
579
|
+
taskPending.push({ mod, entryInfo, url, pkg })
|
|
580
|
+
entries.push(withSourcePath({ type: 'task', id: taskId, entry: url, package: pkg }, entryInfo, projectRoot))
|
|
424
581
|
continue
|
|
425
582
|
}
|
|
426
583
|
|
|
@@ -438,22 +595,39 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
438
595
|
if (mod.slot != null) {
|
|
439
596
|
this.error(
|
|
440
597
|
`[@ossy/app][build] component ${entryInfo.sourcePath} must not export \`slot\`. ` +
|
|
441
|
-
'Assign components to
|
|
598
|
+
'Assign components to app slots in `*.layout.jsx` via `export const slots`.',
|
|
442
599
|
)
|
|
443
600
|
}
|
|
444
|
-
components.push({ id, entry: url, package: entryPackage(entryInfo, appPackageName) })
|
|
601
|
+
components.push(withSourcePath({ id, entry: url, package: entryPackage(entryInfo, appPackageName) }, entryInfo, projectRoot))
|
|
445
602
|
continue
|
|
446
603
|
}
|
|
447
604
|
|
|
448
605
|
if (entryInfo.kind === 'page') {
|
|
449
606
|
const pagePath = rawMeta.path !== undefined ? rawMeta.path : defaultPageRoute(id)
|
|
450
|
-
|
|
607
|
+
reportValidationError(
|
|
608
|
+
this,
|
|
609
|
+
Schema.of({ schemas: [] }).validate(
|
|
610
|
+
CAPABILITY_SCHEMA_ID.PAGE_META,
|
|
611
|
+
{
|
|
612
|
+
id,
|
|
613
|
+
path: pagePath,
|
|
614
|
+
...(rawMeta.title != null ? { title: rawMeta.title } : {}),
|
|
615
|
+
},
|
|
616
|
+
{ sourcePath: entryInfo.sourcePath },
|
|
617
|
+
),
|
|
618
|
+
entryInfo.sourcePath,
|
|
619
|
+
)
|
|
620
|
+
entries.push(withSourcePath({
|
|
451
621
|
type: 'page',
|
|
452
622
|
id,
|
|
453
623
|
path: pagePath,
|
|
454
624
|
title: rawMeta.title,
|
|
455
625
|
entry: url,
|
|
456
626
|
package: entryPackage(entryInfo, appPackageName),
|
|
627
|
+
}, entryInfo, projectRoot))
|
|
628
|
+
pageMetaById.set(id, {
|
|
629
|
+
rawMeta,
|
|
630
|
+
packageName: entryPackage(entryInfo, appPackageName),
|
|
457
631
|
})
|
|
458
632
|
} else if (entryInfo.kind === 'api') {
|
|
459
633
|
// APIs have no sensible default route (unlike pages where we can
|
|
@@ -467,40 +641,158 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
467
641
|
`Add a \`path\` to the metadata or default-export object (e.g. \`path: '/api/${id}'\`).`,
|
|
468
642
|
)
|
|
469
643
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
644
|
+
const apiEntry = {
|
|
645
|
+
type: 'api',
|
|
646
|
+
id,
|
|
647
|
+
path: rawMeta.path,
|
|
648
|
+
entry: url,
|
|
649
|
+
package: entryPackage(entryInfo, appPackageName),
|
|
650
|
+
}
|
|
651
|
+
if (typeof rawMeta.action === 'string' && rawMeta.action.trim()) {
|
|
652
|
+
apiEntry.action = rawMeta.action
|
|
653
|
+
apiEntry.method = rawMeta.method ?? 'GET'
|
|
654
|
+
if (Array.isArray(rawMeta.query)) apiEntry.query = rawMeta.query
|
|
655
|
+
actionRoutes[rawMeta.action] = {
|
|
656
|
+
path: rawMeta.path,
|
|
657
|
+
method: rawMeta.method ?? 'GET',
|
|
658
|
+
...(Array.isArray(rawMeta.query) && rawMeta.query.length ? { query: rawMeta.query } : {}),
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
entries.push(withSourcePath(apiEntry, entryInfo, projectRoot))
|
|
473
662
|
}
|
|
474
663
|
}
|
|
475
664
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
665
|
+
const actionIdSet = new Set(actions.map((a) => a.id))
|
|
666
|
+
const schemaIdSet = new Set(schemas.map((s) => s.id))
|
|
667
|
+
const taskIdList = taskPending.map(({ mod }) => mod?.metadata?.id).filter(Boolean)
|
|
668
|
+
|
|
669
|
+
const schemaEngine = Schema.of(
|
|
670
|
+
{ schemas },
|
|
671
|
+
{ actionIds: [...actionIdSet], taskIds: taskIdList, strictRefs: true },
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
for (const { mod, entryInfo } of taskPending) {
|
|
675
|
+
reportValidationError(
|
|
676
|
+
this,
|
|
677
|
+
schemaEngine.validate(CAPABILITY_SCHEMA_ID.TASK_META, mod?.metadata ?? {}, {
|
|
678
|
+
strictRefs: true,
|
|
679
|
+
schemaIds: schemaIdSet,
|
|
680
|
+
sourcePath: entryInfo.sourcePath,
|
|
681
|
+
}),
|
|
682
|
+
entryInfo.sourcePath,
|
|
480
683
|
)
|
|
481
684
|
}
|
|
482
685
|
|
|
686
|
+
const taskCatalog = taskPending.map(({ mod, url, pkg }) =>
|
|
687
|
+
extractTaskCatalogEntry({
|
|
688
|
+
metadata: mod?.metadata ?? {},
|
|
689
|
+
package: pkg,
|
|
690
|
+
entry: url,
|
|
691
|
+
actionIdsInManifest: actionIdSet,
|
|
692
|
+
}),
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
for (const entry of taskCatalog) {
|
|
696
|
+
reportValidationError(
|
|
697
|
+
this,
|
|
698
|
+
schemaEngine.validate(CAPABILITY_SCHEMA_ID.TASK_CAPABILITY, entry, {
|
|
699
|
+
strictRefs: true,
|
|
700
|
+
schemaIds: schemaIdSet,
|
|
701
|
+
actionIds: actionIdSet,
|
|
702
|
+
allowDerived: true,
|
|
703
|
+
allowUnknownKeys: true,
|
|
704
|
+
label: entry.id,
|
|
705
|
+
}),
|
|
706
|
+
entry.id,
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const taskGraphEdges = buildTaskCatalogEdges(taskCatalog)
|
|
711
|
+
|
|
483
712
|
/** @type {Manifest} */
|
|
484
713
|
const definitions = projectRoot
|
|
485
714
|
? await discoverPackageDefinitions(projectRoot)
|
|
486
715
|
: {}
|
|
487
716
|
|
|
717
|
+
const layoutIdSet = new Set(layouts.map((layout) => layout.id))
|
|
718
|
+
const componentIdSet = new Set(components.map((component) => component.id))
|
|
719
|
+
|
|
720
|
+
for (const entry of entries) {
|
|
721
|
+
if (entry.type !== 'page') continue
|
|
722
|
+
const meta = pageMetaById.get(entry.id)
|
|
723
|
+
const layoutId = resolvePageLayoutId({
|
|
724
|
+
pageMetadata: meta?.rawMeta ?? {},
|
|
725
|
+
appConfig: configValue ?? {},
|
|
726
|
+
})
|
|
727
|
+
if (!layoutIdSet.has(layoutId)) {
|
|
728
|
+
this.error(
|
|
729
|
+
`[@ossy/app][build] page "${entry.id}" references layout "${layoutId}" which is not in manifest.layouts`,
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
if (!isCanonicalLayoutId(layoutId)) {
|
|
733
|
+
this.error(
|
|
734
|
+
`[@ossy/app][build] page "${entry.id}" layout must match @{provider}/{feature}/layout/{concept} — got "${layoutId}"`,
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
entry.layout = layoutId
|
|
738
|
+
const layoutDef = layouts.find((layout) => layout.id === layoutId)
|
|
739
|
+
const mergedSlots = resolvePageShellSlots({
|
|
740
|
+
layoutSlots: layoutDef?.slots,
|
|
741
|
+
appSlots: configValue?.slots,
|
|
742
|
+
pageSlots: meta?.rawMeta?.slots,
|
|
743
|
+
})
|
|
744
|
+
if (Object.keys(mergedSlots).length > 0) {
|
|
745
|
+
entry.slots = mergedSlots
|
|
746
|
+
}
|
|
747
|
+
for (const [slotKey, spec] of Object.entries(mergedSlots)) {
|
|
748
|
+
if (spec?.view === null) continue
|
|
749
|
+
const componentId = typeof spec?.view === 'string' ? spec.view.trim() : ''
|
|
750
|
+
if (!componentId) continue
|
|
751
|
+
if (!componentIdSet.has(componentId)) {
|
|
752
|
+
this.error(
|
|
753
|
+
`[@ossy/app][build] page "${entry.id}" slot "${slotKey}" references unknown component id "${componentId}"`,
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
488
759
|
const manifest = {
|
|
489
760
|
entries,
|
|
490
761
|
components,
|
|
491
|
-
|
|
762
|
+
schemas,
|
|
492
763
|
aggregates,
|
|
493
764
|
integrations,
|
|
494
765
|
startups,
|
|
495
766
|
emails,
|
|
496
767
|
actions,
|
|
768
|
+
forms,
|
|
497
769
|
e2es,
|
|
770
|
+
flows,
|
|
498
771
|
layouts,
|
|
499
772
|
definitions,
|
|
500
773
|
config: configValue || {},
|
|
774
|
+
actionRoutes,
|
|
775
|
+
taskCatalog,
|
|
776
|
+
taskGraphEdges,
|
|
501
777
|
}
|
|
502
778
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
|
|
503
779
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
|
|
780
|
+
|
|
781
|
+
const buildDirPath = path.dirname(manifestPath)
|
|
782
|
+
const tasks = taskCatalog.map((t) => ({ id: t.id }))
|
|
783
|
+
const capabilities = buildCapabilities({
|
|
784
|
+
actions,
|
|
785
|
+
tasks,
|
|
786
|
+
schemas,
|
|
787
|
+
taskCatalog,
|
|
788
|
+
taskGraphEdges,
|
|
789
|
+
})
|
|
790
|
+
const capabilitiesPath = path.join(buildDirPath, 'capabilities.json')
|
|
791
|
+
fs.writeFileSync(capabilitiesPath, JSON.stringify(capabilities, null, 2), 'utf8')
|
|
792
|
+
|
|
793
|
+
const actionsSchemaPath = path.join(buildDirPath, 'actions.schema.json')
|
|
794
|
+
const actionsSchema = buildActionsSchema(actions, { schemas, tasks })
|
|
795
|
+
fs.writeFileSync(actionsSchemaPath, JSON.stringify(actionsSchema, null, 2), 'utf8')
|
|
504
796
|
},
|
|
505
797
|
}
|
|
506
798
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"source": "./src/index.js",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
".": "./src/index.js",
|
|
10
10
|
"./shell": "./src/shell/index.js",
|
|
11
11
|
"./runtime/page-runtime": "./runtime/page-runtime.js",
|
|
12
|
-
"./runtime/resolve-
|
|
12
|
+
"./runtime/resolve-app-slots": "./runtime/resolve-app-slots.js",
|
|
13
13
|
"./runtime/api-runtime": "./runtime/api-runtime.js",
|
|
14
14
|
"./runtime/task-runtime": "./runtime/task-runtime.js",
|
|
15
15
|
"./manifest/build-manifest-summary": "./src/manifest/build-manifest-summary.js",
|
|
16
|
+
"./manifest/build-actions-schema": "./src/manifest/build-actions-schema.js",
|
|
17
|
+
"./manifest/build-capabilities": "./src/manifest/build-capabilities.js",
|
|
16
18
|
"./manifest/discover-package-definitions": "./src/manifest/discover-package-definitions.js",
|
|
17
|
-
"./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"
|
|
18
25
|
},
|
|
19
26
|
"bin": {
|
|
20
27
|
"app": "./cli/index.js"
|
|
@@ -42,15 +49,20 @@
|
|
|
42
49
|
"@babel/eslint-parser": "^7.15.8",
|
|
43
50
|
"@babel/preset-react": "^7.26.3",
|
|
44
51
|
"@babel/register": "^7.25.9",
|
|
45
|
-
"@ossy/
|
|
46
|
-
"@ossy/
|
|
47
|
-
"@ossy/
|
|
48
|
-
"@ossy/
|
|
49
|
-
"@ossy/
|
|
50
|
-
"@ossy/
|
|
51
|
-
"@ossy/
|
|
52
|
-
"@ossy/
|
|
53
|
-
"@ossy/
|
|
52
|
+
"@ossy/authentication": "^3.0.1",
|
|
53
|
+
"@ossy/design-system": "^3.0.1",
|
|
54
|
+
"@ossy/locale": "^3.0.1",
|
|
55
|
+
"@ossy/package-catalog": "^3.0.1",
|
|
56
|
+
"@ossy/pages": "^3.0.1",
|
|
57
|
+
"@ossy/platform": "^3.0.1",
|
|
58
|
+
"@ossy/resources": "^3.0.1",
|
|
59
|
+
"@ossy/router": "^3.0.1",
|
|
60
|
+
"@ossy/router-react": "^3.0.1",
|
|
61
|
+
"@ossy/schema": "^3.0.1",
|
|
62
|
+
"@ossy/sdk": "^3.0.1",
|
|
63
|
+
"@ossy/sdk-react": "^3.0.1",
|
|
64
|
+
"@ossy/themes": "^3.0.1",
|
|
65
|
+
"@ossy/workspaces": "^3.0.1",
|
|
54
66
|
"@rollup/plugin-alias": "^6.0.0",
|
|
55
67
|
"@rollup/plugin-babel": "^7.0.0",
|
|
56
68
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
@@ -84,5 +96,5 @@
|
|
|
84
96
|
"README.md",
|
|
85
97
|
"tsconfig.json"
|
|
86
98
|
],
|
|
87
|
-
"gitHead": "
|
|
99
|
+
"gitHead": "4a70ec216448680d2fddc57b2a8a0077489720ae"
|
|
88
100
|
}
|