@ossy/app 1.40.2 → 1.40.3
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 +43 -12
- package/cli/build.task.js +49 -10
- package/cli/get-platform-files.task.js +17 -9
- package/cli/manifest-plugin.js +250 -25
- package/package.json +15 -11
- package/runtime/page-runtime.js +5 -3
- package/runtime/resolve-app-slots.js +89 -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/template-to-json-schema.js +103 -0
- package/src/shell/App.jsx +6 -3
- package/src/shell/AppSettings.jsx +11 -0
- package/src/shell/DevPagesPanel.jsx +17 -13
- package/src/shell/ThemeEditor.jsx +18 -33
- package/src/shell/WorkspaceAppSettingsSync.jsx +39 -0
- package/src/shell/index.js +5 -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/useShellWorkspace.js +40 -0
- package/runtime/resolve-shell-slots.js +0 -112
package/cli/manifest-plugin.js
CHANGED
|
@@ -4,6 +4,16 @@ 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'
|
|
7
17
|
|
|
8
18
|
/**
|
|
9
19
|
* @typedef {import('./get-platform-files.task.js').PlatformEntry} PlatformEntry
|
|
@@ -62,14 +72,19 @@ import { discoverPackageDefinitions } from '../src/manifest/discover-package-def
|
|
|
62
72
|
* @property {string} entry URL the platform serves the startup bundle from.
|
|
63
73
|
*
|
|
64
74
|
* @typedef {object} EmailManifestEntry
|
|
65
|
-
* @property {string} id Unique email id (e.g. `'authentication/verify-sign-in'`).
|
|
75
|
+
* @property {string} id Unique email id (e.g. `'@ossy/authentication/emails/verify-sign-in'`).
|
|
66
76
|
* @property {string} entry URL the platform serves the email bundle from.
|
|
67
77
|
*
|
|
68
78
|
* @typedef {object} ActionManifestEntry
|
|
69
|
-
* @property {string} id Unique action slug (e.g. `'authentication/request-sign-in'`).
|
|
79
|
+
* @property {string} id Unique action slug (e.g. `'@ossy/authentication/actions/request-sign-in'`).
|
|
70
80
|
* @property {string} entry URL the platform serves the action bundle from.
|
|
71
81
|
* @property {string} access Access level: `'public'` | `'authenticated'` | `'workspace'`.
|
|
72
82
|
*
|
|
83
|
+
* @typedef {object} FormManifestEntry
|
|
84
|
+
* @property {string} id Unique form slug (e.g. `'@ossy/authentication/form/sign-up'`).
|
|
85
|
+
* @property {string} schemaId Resource schema id (e.g. `'@ossy/authentication/sign-up'`).
|
|
86
|
+
* @property {string} entry URL the platform serves the form bundle from.
|
|
87
|
+
*
|
|
73
88
|
* @typedef {object} LayoutManifestEntry
|
|
74
89
|
* @property {string} id Unique layout id (e.g. `'app-shell'`).
|
|
75
90
|
* @property {string} entry URL the platform serves the layout bundle from.
|
|
@@ -82,16 +97,24 @@ import { discoverPackageDefinitions } from '../src/manifest/discover-package-def
|
|
|
82
97
|
* @property {string[]} requires Runtime dependencies needed (e.g. `['server', 'database']`).
|
|
83
98
|
* @property {string} entry URL the platform serves the e2e bundle from.
|
|
84
99
|
*
|
|
100
|
+
* @typedef {object} FlowManifestEntry
|
|
101
|
+
* @property {string} id Unique flow id (e.g. `'@ossy/authentication/actions/sign-up'`).
|
|
102
|
+
* @property {string} feature Grouping label (e.g. `'authentication'`).
|
|
103
|
+
* @property {string[]} requires Runtime dependencies needed (e.g. `['server', 'database']`).
|
|
104
|
+
* @property {string} entry URL the platform serves the flow bundle from.
|
|
105
|
+
*
|
|
85
106
|
* @typedef {object} Manifest
|
|
86
107
|
* @property {ManifestEntry[]} entries Flat, ordered list of every routable thing.
|
|
87
108
|
* @property {ComponentManifestEntry[]} components Discovered `*.component.*` entries, keyed by id.
|
|
88
|
-
* @property {object[]}
|
|
109
|
+
* @property {object[]} schemas Each `*.schema.js` file's default-exported template object.
|
|
89
110
|
* @property {AggregateManifestEntry[]} aggregates Discovered `*.aggregate.js` entries from installed packages.
|
|
90
111
|
* @property {IntegrationManifestEntry[]} integrations Discovered `*.integration.js` entries.
|
|
91
112
|
* @property {StartupManifestEntry[]} startups Discovered `*.startup.js` entries.
|
|
92
113
|
* @property {EmailManifestEntry[]} emails Discovered `*.email.{jsx,tsx}` entries.
|
|
93
114
|
* @property {ActionManifestEntry[]} actions Discovered `*.action.js` entries.
|
|
115
|
+
* @property {FormManifestEntry[]} forms Discovered `*.form.js` entries.
|
|
94
116
|
* @property {E2eManifestEntry[]} e2es Discovered `*.e2e.js` entries from installed packages.
|
|
117
|
+
* @property {FlowManifestEntry[]} flows Discovered `*.flow.js` entries from installed packages.
|
|
95
118
|
* @property {LayoutManifestEntry[]} layouts Discovered `*.layout.{jsx,tsx}` entries (includes app `slots` map).
|
|
96
119
|
* @property {object} config Inlined `src/config.js` default export.
|
|
97
120
|
* @property {Record<string, object>} [definitions] Package Definition metadata keyed by slug.
|
|
@@ -131,6 +154,12 @@ import { discoverPackageDefinitions } from '../src/manifest/discover-package-def
|
|
|
131
154
|
* @param {ManifestPluginOptions} options
|
|
132
155
|
* @returns {import('rollup').Plugin}
|
|
133
156
|
*/
|
|
157
|
+
function reportValidationError (plugin, result, sourcePath) {
|
|
158
|
+
if (result.ok) return
|
|
159
|
+
const message = result.errors?.[0]?.message ?? result.message ?? 'Validation failed'
|
|
160
|
+
plugin.error(`[@ossy/app][build] ${sourcePath}: ${message}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
134
163
|
function entryPackage (entryInfo, appPackageName) {
|
|
135
164
|
return entryInfo.packageName || appPackageName
|
|
136
165
|
}
|
|
@@ -141,8 +170,10 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
141
170
|
async writeBundle (_, bundle) {
|
|
142
171
|
/** @type {ManifestEntry[]} */
|
|
143
172
|
const entries = []
|
|
173
|
+
/** @type {Record<string, { path: string, method?: string, query?: string[] }>} */
|
|
174
|
+
const actionRoutes = {}
|
|
144
175
|
/** @type {object[]} */
|
|
145
|
-
const
|
|
176
|
+
const schemas = []
|
|
146
177
|
/** @type {import('./get-platform-files.task.js').ComponentManifestEntry[]} */
|
|
147
178
|
const components = []
|
|
148
179
|
/** @type {AggregateManifestEntry[]} */
|
|
@@ -155,6 +186,8 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
155
186
|
const emails = []
|
|
156
187
|
/** @type {ActionManifestEntry[]} */
|
|
157
188
|
const actions = []
|
|
189
|
+
/** @type {FormManifestEntry[]} */
|
|
190
|
+
const forms = []
|
|
158
191
|
const seenIds = { page: new Set(), api: new Set(), task: new Set(), component: new Set() }
|
|
159
192
|
const seenResourceIds = new Set()
|
|
160
193
|
const seenAggregateIds = new Set()
|
|
@@ -162,12 +195,18 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
162
195
|
const seenStartupIds = new Set()
|
|
163
196
|
const seenEmailIds = new Set()
|
|
164
197
|
const seenActionIds = new Set()
|
|
198
|
+
const seenFormIds = new Set()
|
|
165
199
|
/** @type {E2eManifestEntry[]} */
|
|
166
200
|
const e2es = []
|
|
167
201
|
const seenE2eIds = new Set()
|
|
202
|
+
/** @type {FlowManifestEntry[]} */
|
|
203
|
+
const flows = []
|
|
204
|
+
const seenFlowIds = new Set()
|
|
168
205
|
/** @type {LayoutManifestEntry[]} */
|
|
169
206
|
const layouts = []
|
|
170
207
|
const seenLayoutIds = new Set()
|
|
208
|
+
/** @type {Array<{ mod: object, entryInfo: import('./get-platform-files.task.js').PlatformEntry, url: string, pkg: string }>} */
|
|
209
|
+
const taskPending = []
|
|
171
210
|
for (const fileName of Object.keys(bundle)) {
|
|
172
211
|
const chunk = bundle[fileName]
|
|
173
212
|
if (chunk.type !== 'chunk' || !chunk.isEntry) continue
|
|
@@ -268,8 +307,8 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
268
307
|
continue
|
|
269
308
|
}
|
|
270
309
|
|
|
271
|
-
// Actions are intent: `metadata` only (`{ id, access }`).
|
|
272
|
-
//
|
|
310
|
+
// Actions are intent: `metadata` only (`{ id, access }`). Primary implementation
|
|
311
|
+
// lives in `{feature}/tasks/{intent}` derived from the action id (ADR 0003).
|
|
273
312
|
if (entryInfo.kind === 'action') {
|
|
274
313
|
const actionId = mod?.metadata?.id
|
|
275
314
|
if (typeof actionId !== 'string' || actionId.trim() === '') {
|
|
@@ -281,11 +320,47 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
281
320
|
this.error(`[@ossy/app][build] Duplicate action id "${actionId}"`)
|
|
282
321
|
}
|
|
283
322
|
const access = mod.metadata?.access ?? 'authenticated'
|
|
323
|
+
reportValidationError(
|
|
324
|
+
this,
|
|
325
|
+
Schema.of({ schemas: [] }).validate(
|
|
326
|
+
CAPABILITY_SCHEMA_ID.ACTION_META,
|
|
327
|
+
{ id: actionId, ...(mod.metadata?.access != null ? { access: mod.metadata.access } : {}) },
|
|
328
|
+
{ sourcePath: entryInfo.sourcePath },
|
|
329
|
+
),
|
|
330
|
+
entryInfo.sourcePath,
|
|
331
|
+
)
|
|
284
332
|
seenActionIds.add(actionId)
|
|
285
333
|
actions.push({ id: actionId, entry: url, access, package: entryPackage(entryInfo, appPackageName) })
|
|
286
334
|
continue
|
|
287
335
|
}
|
|
288
336
|
|
|
337
|
+
// Forms are intent metadata (`metadata` only) referencing a resource template.
|
|
338
|
+
if (entryInfo.kind === 'form') {
|
|
339
|
+
const formId = mod?.metadata?.id
|
|
340
|
+
const schemaId = mod?.metadata?.schemaId
|
|
341
|
+
if (typeof formId !== 'string' || formId.trim() === '') {
|
|
342
|
+
this.error(
|
|
343
|
+
`[@ossy/app][build] form entry ${entryInfo.sourcePath} must export a non-empty string "metadata.id"`,
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
if (typeof schemaId !== 'string' || schemaId.trim() === '') {
|
|
347
|
+
this.error(
|
|
348
|
+
`[@ossy/app][build] form entry ${entryInfo.sourcePath} must export a non-empty string "metadata.schemaId"`,
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
if (seenFormIds.has(formId)) {
|
|
352
|
+
this.error(`[@ossy/app][build] Duplicate form id "${formId}"`)
|
|
353
|
+
}
|
|
354
|
+
seenFormIds.add(formId)
|
|
355
|
+
forms.push({
|
|
356
|
+
id: formId,
|
|
357
|
+
schemaId,
|
|
358
|
+
entry: url,
|
|
359
|
+
package: entryPackage(entryInfo, appPackageName),
|
|
360
|
+
})
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
|
|
289
364
|
// Aggregates are server-side classes — the bundle exports `Aggregate`
|
|
290
365
|
// (the class) and `id` (the AggregateType string). They don't produce
|
|
291
366
|
// routable entries; they accumulate into a separate `aggregates` array.
|
|
@@ -344,6 +419,36 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
344
419
|
continue
|
|
345
420
|
}
|
|
346
421
|
|
|
422
|
+
if (entryInfo.kind === 'flow') {
|
|
423
|
+
const flowMeta = mod?.metadata ?? mod?.default?.metadata ?? {}
|
|
424
|
+
const flowId = flowMeta.id ?? mod?.default?.id
|
|
425
|
+
const feature = flowMeta.feature
|
|
426
|
+
const requires = flowMeta.requires
|
|
427
|
+
const flowBody = mod?.default
|
|
428
|
+
if (typeof flowId !== 'string' || flowId.trim() === '') {
|
|
429
|
+
this.error(
|
|
430
|
+
`[@ossy/app][build] flow entry ${entryInfo.sourcePath} must export metadata.id`,
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
if (!flowBody || typeof flowBody !== 'object' || !Array.isArray(flowBody.steps)) {
|
|
434
|
+
this.error(
|
|
435
|
+
`[@ossy/app][build] flow entry ${entryInfo.sourcePath} must default-export an object with steps[]`,
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
if (seenFlowIds.has(flowId)) {
|
|
439
|
+
this.error(`[@ossy/app][build] Duplicate flow id "${flowId}"`)
|
|
440
|
+
}
|
|
441
|
+
seenFlowIds.add(flowId)
|
|
442
|
+
flows.push({
|
|
443
|
+
id: flowId,
|
|
444
|
+
feature: typeof feature === 'string' ? feature : '',
|
|
445
|
+
requires: Array.isArray(requires) ? requires : [],
|
|
446
|
+
entry: url,
|
|
447
|
+
package: entryPackage(entryInfo, appPackageName),
|
|
448
|
+
})
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
|
|
347
452
|
// Layouts wrap every page render. The bundle exports `id` and a default
|
|
348
453
|
// export (the React component). Only one `*.layout.jsx` per app is
|
|
349
454
|
// supported. They don't produce routable entries; they accumulate into
|
|
@@ -400,27 +505,51 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
400
505
|
continue
|
|
401
506
|
}
|
|
402
507
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// `resourceTemplates` array on the manifest.
|
|
407
|
-
if (entryInfo.kind === 'resource') {
|
|
408
|
-
const template = mod && mod.default
|
|
409
|
-
if (!template || typeof template !== 'object' || Array.isArray(template)) {
|
|
508
|
+
if (entryInfo.kind === 'schema') {
|
|
509
|
+
const schema = mod && mod.default
|
|
510
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
410
511
|
this.error(
|
|
411
|
-
`[@ossy/app][build]
|
|
512
|
+
`[@ossy/app][build] schema entry ${entryInfo.sourcePath} must default-export an object`,
|
|
412
513
|
)
|
|
413
514
|
}
|
|
414
|
-
if (typeof
|
|
515
|
+
if (typeof schema.id !== 'string' || schema.id.trim() === '') {
|
|
415
516
|
this.error(
|
|
416
|
-
`[@ossy/app][build]
|
|
517
|
+
`[@ossy/app][build] schema entry ${entryInfo.sourcePath} default export is missing a non-empty string "id"`,
|
|
417
518
|
)
|
|
418
519
|
}
|
|
419
|
-
if (
|
|
420
|
-
this.error(
|
|
520
|
+
if (!isCanonicalSchemaId(schema.id)) {
|
|
521
|
+
this.error(
|
|
522
|
+
`[@ossy/app][build] schema entry ${entryInfo.sourcePath} id must match @{provider}/{feature}/schema/{concept} — got "${schema.id}"`,
|
|
523
|
+
)
|
|
421
524
|
}
|
|
422
|
-
seenResourceIds.
|
|
423
|
-
|
|
525
|
+
if (seenResourceIds.has(schema.id)) {
|
|
526
|
+
this.error(`[@ossy/app][build] Duplicate schema id "${schema.id}"`)
|
|
527
|
+
}
|
|
528
|
+
seenResourceIds.add(schema.id)
|
|
529
|
+
schemas.push({ ...schema, package: entryPackage(entryInfo, appPackageName) })
|
|
530
|
+
continue
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (entryInfo.kind === 'task') {
|
|
534
|
+
const taskMeta = mod?.metadata
|
|
535
|
+
const taskId = taskMeta?.id
|
|
536
|
+
if (typeof taskId !== 'string' || taskId.trim() === '') {
|
|
537
|
+
this.error(
|
|
538
|
+
`[@ossy/app][build] task entry ${entryInfo.sourcePath} must export a non-empty string "metadata.id"`,
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
if (isActionId(taskId)) {
|
|
542
|
+
this.error(
|
|
543
|
+
`[@ossy/app][build] Task "${taskId}" uses /actions/ — use /tasks/ (e.g. "${taskIdFromActionId(taskId)}")`,
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
if (seenIds.task.has(taskId)) {
|
|
547
|
+
this.error(`[@ossy/app][build] Duplicate task id "${taskId}"`)
|
|
548
|
+
}
|
|
549
|
+
seenIds.task.add(taskId)
|
|
550
|
+
const pkg = entryPackage(entryInfo, appPackageName)
|
|
551
|
+
taskPending.push({ mod, entryInfo, url, pkg })
|
|
552
|
+
entries.push({ type: 'task', id: taskId, entry: url, package: pkg })
|
|
424
553
|
continue
|
|
425
554
|
}
|
|
426
555
|
|
|
@@ -438,7 +567,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
438
567
|
if (mod.slot != null) {
|
|
439
568
|
this.error(
|
|
440
569
|
`[@ossy/app][build] component ${entryInfo.sourcePath} must not export \`slot\`. ` +
|
|
441
|
-
'Assign components to
|
|
570
|
+
'Assign components to app slots in `*.layout.jsx` via `export const slots`.',
|
|
442
571
|
)
|
|
443
572
|
}
|
|
444
573
|
components.push({ id, entry: url, package: entryPackage(entryInfo, appPackageName) })
|
|
@@ -447,6 +576,19 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
447
576
|
|
|
448
577
|
if (entryInfo.kind === 'page') {
|
|
449
578
|
const pagePath = rawMeta.path !== undefined ? rawMeta.path : defaultPageRoute(id)
|
|
579
|
+
reportValidationError(
|
|
580
|
+
this,
|
|
581
|
+
Schema.of({ schemas: [] }).validate(
|
|
582
|
+
CAPABILITY_SCHEMA_ID.PAGE_META,
|
|
583
|
+
{
|
|
584
|
+
id,
|
|
585
|
+
path: pagePath,
|
|
586
|
+
...(rawMeta.title != null ? { title: rawMeta.title } : {}),
|
|
587
|
+
},
|
|
588
|
+
{ sourcePath: entryInfo.sourcePath },
|
|
589
|
+
),
|
|
590
|
+
entryInfo.sourcePath,
|
|
591
|
+
)
|
|
450
592
|
entries.push({
|
|
451
593
|
type: 'page',
|
|
452
594
|
id,
|
|
@@ -467,12 +609,74 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
467
609
|
`Add a \`path\` to the metadata or default-export object (e.g. \`path: '/api/${id}'\`).`,
|
|
468
610
|
)
|
|
469
611
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
612
|
+
const apiEntry = {
|
|
613
|
+
type: 'api',
|
|
614
|
+
id,
|
|
615
|
+
path: rawMeta.path,
|
|
616
|
+
entry: url,
|
|
617
|
+
package: entryPackage(entryInfo, appPackageName),
|
|
618
|
+
}
|
|
619
|
+
if (typeof rawMeta.action === 'string' && rawMeta.action.trim()) {
|
|
620
|
+
apiEntry.action = rawMeta.action
|
|
621
|
+
apiEntry.method = rawMeta.method ?? 'GET'
|
|
622
|
+
if (Array.isArray(rawMeta.query)) apiEntry.query = rawMeta.query
|
|
623
|
+
actionRoutes[rawMeta.action] = {
|
|
624
|
+
path: rawMeta.path,
|
|
625
|
+
method: rawMeta.method ?? 'GET',
|
|
626
|
+
...(Array.isArray(rawMeta.query) && rawMeta.query.length ? { query: rawMeta.query } : {}),
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
entries.push(apiEntry)
|
|
473
630
|
}
|
|
474
631
|
}
|
|
475
632
|
|
|
633
|
+
const actionIdSet = new Set(actions.map((a) => a.id))
|
|
634
|
+
const schemaIdSet = new Set(schemas.map((s) => s.id))
|
|
635
|
+
const taskIdList = taskPending.map(({ mod }) => mod?.metadata?.id).filter(Boolean)
|
|
636
|
+
|
|
637
|
+
const schemaEngine = Schema.of(
|
|
638
|
+
{ schemas },
|
|
639
|
+
{ actionIds: [...actionIdSet], taskIds: taskIdList, strictRefs: true },
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
for (const { mod, entryInfo } of taskPending) {
|
|
643
|
+
reportValidationError(
|
|
644
|
+
this,
|
|
645
|
+
schemaEngine.validate(CAPABILITY_SCHEMA_ID.TASK_META, mod?.metadata ?? {}, {
|
|
646
|
+
strictRefs: true,
|
|
647
|
+
schemaIds: schemaIdSet,
|
|
648
|
+
sourcePath: entryInfo.sourcePath,
|
|
649
|
+
}),
|
|
650
|
+
entryInfo.sourcePath,
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const taskCatalog = taskPending.map(({ mod, url, pkg }) =>
|
|
655
|
+
extractTaskCatalogEntry({
|
|
656
|
+
metadata: mod?.metadata ?? {},
|
|
657
|
+
package: pkg,
|
|
658
|
+
entry: url,
|
|
659
|
+
actionIdsInManifest: actionIdSet,
|
|
660
|
+
}),
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
for (const entry of taskCatalog) {
|
|
664
|
+
reportValidationError(
|
|
665
|
+
this,
|
|
666
|
+
schemaEngine.validate(CAPABILITY_SCHEMA_ID.TASK_CAPABILITY, entry, {
|
|
667
|
+
strictRefs: true,
|
|
668
|
+
schemaIds: schemaIdSet,
|
|
669
|
+
actionIds: actionIdSet,
|
|
670
|
+
allowDerived: true,
|
|
671
|
+
allowUnknownKeys: true,
|
|
672
|
+
label: entry.id,
|
|
673
|
+
}),
|
|
674
|
+
entry.id,
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const taskGraphEdges = buildTaskCatalogEdges(taskCatalog)
|
|
679
|
+
|
|
476
680
|
if (layouts.length > 1) {
|
|
477
681
|
this.error(
|
|
478
682
|
`[@ossy/app][build] Only one *.layout.jsx per app is allowed (found ${layouts.length}). ` +
|
|
@@ -488,19 +692,40 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
488
692
|
const manifest = {
|
|
489
693
|
entries,
|
|
490
694
|
components,
|
|
491
|
-
|
|
695
|
+
schemas,
|
|
492
696
|
aggregates,
|
|
493
697
|
integrations,
|
|
494
698
|
startups,
|
|
495
699
|
emails,
|
|
496
700
|
actions,
|
|
701
|
+
forms,
|
|
497
702
|
e2es,
|
|
703
|
+
flows,
|
|
498
704
|
layouts,
|
|
499
705
|
definitions,
|
|
500
706
|
config: configValue || {},
|
|
707
|
+
actionRoutes,
|
|
708
|
+
taskCatalog,
|
|
709
|
+
taskGraphEdges,
|
|
501
710
|
}
|
|
502
711
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
|
|
503
712
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
|
|
713
|
+
|
|
714
|
+
const buildDirPath = path.dirname(manifestPath)
|
|
715
|
+
const tasks = taskCatalog.map((t) => ({ id: t.id }))
|
|
716
|
+
const capabilities = buildCapabilities({
|
|
717
|
+
actions,
|
|
718
|
+
tasks,
|
|
719
|
+
schemas,
|
|
720
|
+
taskCatalog,
|
|
721
|
+
taskGraphEdges,
|
|
722
|
+
})
|
|
723
|
+
const capabilitiesPath = path.join(buildDirPath, 'capabilities.json')
|
|
724
|
+
fs.writeFileSync(capabilitiesPath, JSON.stringify(capabilities, null, 2), 'utf8')
|
|
725
|
+
|
|
726
|
+
const actionsSchemaPath = path.join(buildDirPath, 'actions.schema.json')
|
|
727
|
+
const actionsSchema = buildActionsSchema(actions, { schemas, tasks })
|
|
728
|
+
fs.writeFileSync(actionsSchemaPath, JSON.stringify(actionsSchema, null, 2), 'utf8')
|
|
504
729
|
},
|
|
505
730
|
}
|
|
506
731
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/app",
|
|
3
|
-
"version": "1.40.
|
|
3
|
+
"version": "1.40.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"source": "./src/index.js",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -9,10 +9,12 @@
|
|
|
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
19
|
"./manifest/serialize-package-definition": "./src/manifest/serialize-package-definition.js"
|
|
18
20
|
},
|
|
@@ -42,15 +44,17 @@
|
|
|
42
44
|
"@babel/eslint-parser": "^7.15.8",
|
|
43
45
|
"@babel/preset-react": "^7.26.3",
|
|
44
46
|
"@babel/register": "^7.25.9",
|
|
45
|
-
"@ossy/design-system": "^1.40.
|
|
46
|
-
"@ossy/locale": "^1.40.
|
|
47
|
+
"@ossy/design-system": "^1.40.3",
|
|
48
|
+
"@ossy/locale": "^1.40.3",
|
|
47
49
|
"@ossy/pages": "^1.23.0",
|
|
48
|
-
"@ossy/platform": "^1.39.
|
|
49
|
-
"@ossy/router": "^1.40.
|
|
50
|
-
"@ossy/router-react": "^1.40.
|
|
51
|
-
"@ossy/
|
|
52
|
-
"@ossy/sdk
|
|
53
|
-
"@ossy/
|
|
50
|
+
"@ossy/platform": "^1.39.3",
|
|
51
|
+
"@ossy/router": "^1.40.3",
|
|
52
|
+
"@ossy/router-react": "^1.40.3",
|
|
53
|
+
"@ossy/schema": "^1.0.1",
|
|
54
|
+
"@ossy/sdk": "^1.40.3",
|
|
55
|
+
"@ossy/sdk-react": "^1.40.3",
|
|
56
|
+
"@ossy/themes": "^1.40.3",
|
|
57
|
+
"@ossy/workspaces": "^1.17.3",
|
|
54
58
|
"@rollup/plugin-alias": "^6.0.0",
|
|
55
59
|
"@rollup/plugin-babel": "^7.0.0",
|
|
56
60
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
@@ -84,5 +88,5 @@
|
|
|
84
88
|
"README.md",
|
|
85
89
|
"tsconfig.json"
|
|
86
90
|
],
|
|
87
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "a0d89185a17f8de8ce328c3a648c108ff1d61d8f"
|
|
88
92
|
}
|
package/runtime/page-runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createElement } from 'react'
|
|
2
2
|
import { pageIdToDocumentTitleKey, resolveMessage } from '@ossy/locale'
|
|
3
3
|
import { App } from '../src/shell/App.jsx'
|
|
4
|
-
import { resolvePageSlots } from './resolve-
|
|
4
|
+
import { resolvePageSlots } from './resolve-app-slots.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Dynamically imports each component bundle listed in `entries` and returns a
|
|
@@ -129,10 +129,11 @@ export function createPageEntry (pageModule, options = {}) {
|
|
|
129
129
|
|
|
130
130
|
const { Layout = null, componentEntries = [], layoutSlots = {}, ...pageProps } = props
|
|
131
131
|
const componentsById = await loadComponents(componentEntries)
|
|
132
|
+
const PageContent = (slotProps = {}) => createElement(Component, { ...pageProps, ...slotProps })
|
|
132
133
|
const components = resolvePageSlots({
|
|
133
134
|
layoutSlots,
|
|
134
135
|
componentsById,
|
|
135
|
-
pageComponent:
|
|
136
|
+
pageComponent: PageContent,
|
|
136
137
|
})
|
|
137
138
|
const tree = buildTree({ Component, Layout, metadata, props: { ...pageProps, components } })
|
|
138
139
|
const bootstrapUrl = toBootstrapUrl(entryUrl)
|
|
@@ -175,10 +176,11 @@ export function createPageEntry (pageModule, options = {}) {
|
|
|
175
176
|
loadComponents(componentEntries),
|
|
176
177
|
loadLayout(layoutEntry),
|
|
177
178
|
]).then(([{ hydrateRoot }, componentsById, Layout]) => {
|
|
179
|
+
const PageContent = (slotProps = {}) => createElement(Component, { ...pageProps, ...slotProps })
|
|
178
180
|
const components = resolvePageSlots({
|
|
179
181
|
layoutSlots,
|
|
180
182
|
componentsById,
|
|
181
|
-
pageComponent:
|
|
183
|
+
pageComponent: PageContent,
|
|
182
184
|
})
|
|
183
185
|
hydrateRoot(document, buildTree({ Component, Layout, metadata, props: { ...pageProps, components } }))
|
|
184
186
|
}).catch((err) => {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Canonical app chrome slot names (namespaced). App layout maps chrome only — not content. */
|
|
2
|
+
export const APP_SLOT_NAMES = [
|
|
3
|
+
'app:header',
|
|
4
|
+
'app:sidebar',
|
|
5
|
+
'app:toolbar',
|
|
6
|
+
'app:notifications',
|
|
7
|
+
'app:system-messages',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
/** Platform-owned slot filled with the current route page component. */
|
|
11
|
+
export const CONTENT_SLOT_NAME = 'app:content'
|
|
12
|
+
|
|
13
|
+
const APP_SLOT_PREFIX = 'app'
|
|
14
|
+
|
|
15
|
+
/** Bare app region → namespaced key (legacy fallback for app components only). */
|
|
16
|
+
const BARE_APP_REGION = {
|
|
17
|
+
header: 'app:header',
|
|
18
|
+
sidebar: 'app:sidebar',
|
|
19
|
+
toolbar: 'app:toolbar',
|
|
20
|
+
notifications: 'app:notifications',
|
|
21
|
+
'system-messages': 'app:system-messages',
|
|
22
|
+
content: CONTENT_SLOT_NAME,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a slot map key to the canonical namespaced form when it is a bare app region.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} slotName
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeAppSlotName (slotName) {
|
|
32
|
+
const key = typeof slotName === 'string' ? slotName.trim() : ''
|
|
33
|
+
return BARE_APP_REGION[key] ?? key
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build `Record<slotName, Component>` from the app layout's static `slots` map
|
|
38
|
+
* (`slotName → componentId`) and components loaded by `metadata.id`.
|
|
39
|
+
*
|
|
40
|
+
* @param {Record<string, string> | null | undefined} layoutSlotsMap
|
|
41
|
+
* @param {Record<string, import('react').ComponentType>} componentsById
|
|
42
|
+
* @returns {Record<string, import('react').ComponentType>}
|
|
43
|
+
*/
|
|
44
|
+
export function resolveAppSlots (layoutSlotsMap, componentsById) {
|
|
45
|
+
/** @type {Record<string, import('react').ComponentType>} */
|
|
46
|
+
const resolved = {}
|
|
47
|
+
|
|
48
|
+
const map = layoutSlotsMap && typeof layoutSlotsMap === 'object' ? layoutSlotsMap : {}
|
|
49
|
+
for (const [rawSlot, componentId] of Object.entries(map)) {
|
|
50
|
+
const slotName = normalizeAppSlotName(rawSlot)
|
|
51
|
+
const id = typeof componentId === 'string' ? componentId.trim() : ''
|
|
52
|
+
if (!slotName || !id || slotName === CONTENT_SLOT_NAME) continue
|
|
53
|
+
const Component = componentsById[id]
|
|
54
|
+
if (Component) resolved[slotName] = Component
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const slotName of APP_SLOT_NAMES) {
|
|
58
|
+
if (resolved[slotName]) continue
|
|
59
|
+
if (componentsById[slotName]) {
|
|
60
|
+
resolved[slotName] = componentsById[slotName]
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
const bare = slotName.slice(`${APP_SLOT_PREFIX}:`.length)
|
|
64
|
+
if (componentsById[bare]) resolved[slotName] = componentsById[bare]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return resolved
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Full provider slot map for a page request: app chrome and page content.
|
|
72
|
+
* Feature components register at canonical ADR 0006 ids (`@ossy/…/view|form/…`).
|
|
73
|
+
*
|
|
74
|
+
* @param {{
|
|
75
|
+
* layoutSlots?: Record<string, string> | null,
|
|
76
|
+
* componentsById?: Record<string, import('react').ComponentType>,
|
|
77
|
+
* pageComponent?: import('react').ComponentType | null,
|
|
78
|
+
* }} options
|
|
79
|
+
* @returns {Record<string, import('react').ComponentType>}
|
|
80
|
+
*/
|
|
81
|
+
export function resolvePageSlots ({ layoutSlots, componentsById = {}, pageComponent = null } = {}) {
|
|
82
|
+
const appSlots = resolveAppSlots(layoutSlots, componentsById)
|
|
83
|
+
/** @type {Record<string, import('react').ComponentType>} */
|
|
84
|
+
const resolved = { ...componentsById, ...appSlots }
|
|
85
|
+
if (pageComponent) {
|
|
86
|
+
resolved[CONTENT_SLOT_NAME] = pageComponent
|
|
87
|
+
}
|
|
88
|
+
return resolved
|
|
89
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive MCP tool name from platform action id.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* actionIdToToolName('@ossy/resources/actions/list') // 'ossy_resources_list'
|
|
6
|
+
*
|
|
7
|
+
* @param {string} actionId
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function actionIdToToolName (actionId) {
|
|
11
|
+
const parts = actionId.split('/')
|
|
12
|
+
if (parts.length >= 4 && parts[0].startsWith('@') && parts[2] === 'actions') {
|
|
13
|
+
const provider = parts[0].slice(1)
|
|
14
|
+
return `${provider}_${parts[1]}_${parts[3]}`.replace(/-/g, '_')
|
|
15
|
+
}
|
|
16
|
+
return `ossy_${actionId.replace(/^@/, '').replace(/\//g, '_').replace(/-/g, '_')}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} actionId
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function humanizeActionId (actionId) {
|
|
24
|
+
const intent = actionId.split('/').pop() || actionId
|
|
25
|
+
return intent
|
|
26
|
+
.split('-')
|
|
27
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
28
|
+
.join(' ')
|
|
29
|
+
}
|