@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.
Files changed (47) hide show
  1. package/README.md +49 -12
  2. package/cli/build.task.js +49 -10
  3. package/cli/get-platform-files.task.js +21 -15
  4. package/cli/manifest-plugin.js +340 -48
  5. package/package.json +25 -13
  6. package/runtime/merge-shell-slots.js +148 -0
  7. package/runtime/page-runtime.js +36 -18
  8. package/runtime/resolve-app-slots.js +126 -0
  9. package/src/en.translations.json +8 -0
  10. package/src/manifest/action-id-to-tool-name.js +29 -0
  11. package/src/manifest/build-action-input-schema.js +220 -0
  12. package/src/manifest/build-actions-schema.js +36 -0
  13. package/src/manifest/build-capabilities.js +190 -0
  14. package/src/manifest/build-manifest-summary.js +11 -5
  15. package/src/manifest/extract-task-catalog.js +1 -0
  16. package/src/manifest/resolve-page-layout.js +51 -0
  17. package/src/manifest/serialize-package-definition.js +2 -3
  18. package/src/manifest/template-to-json-schema.js +103 -0
  19. package/src/shell/App.jsx +18 -9
  20. package/src/shell/AppSettings.jsx +17 -0
  21. package/src/shell/DevPagesPanel.jsx +17 -13
  22. package/src/shell/HeaderAuthActions.jsx +49 -0
  23. package/src/shell/LanguageList.jsx +83 -0
  24. package/src/shell/LanguageSelect.jsx +73 -0
  25. package/src/shell/ThemeEditor.jsx +19 -34
  26. package/src/shell/ThemeSelect.jsx +101 -0
  27. package/src/shell/WorkspaceAppSettingsSync.jsx +39 -0
  28. package/src/shell/buildSidebarNav.js +113 -0
  29. package/src/shell/index.js +11 -0
  30. package/src/shell/languageCode.js +31 -0
  31. package/src/shell/patchUserAppSettings.js +10 -0
  32. package/src/shell/resolveEndpoints.js +43 -0
  33. package/src/shell/resolveWorkspaceServices.js +20 -0
  34. package/src/shell/themeEditorStyles.js +49 -0
  35. package/src/shell/useCompactShellLayout.js +24 -0
  36. package/src/shell/useShellWorkspace.js +40 -0
  37. package/src/shell-registry/blank.layout.jsx +8 -0
  38. package/src/shell-registry/default.layout.jsx +101 -0
  39. package/src/shell-registry/footer-default.component.jsx +39 -0
  40. package/src/shell-registry/head-default.component.jsx +17 -0
  41. package/src/shell-registry/header-default.component.jsx +50 -0
  42. package/src/shell-registry/logo-logomark.component.jsx +8 -0
  43. package/src/shell-registry/logo-logotype.component.jsx +15 -0
  44. package/src/shell-registry/minimal.layout.jsx +58 -0
  45. package/src/shell-registry/sidebar-default.component.jsx +281 -0
  46. package/src/sv.translations.json +8 -0
  47. package/runtime/resolve-shell-slots.js +0 -112
@@ -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 Unique layout id (e.g. `'app-shell'`).
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[]} resourceTemplates Each `*.resource.js` file's default-exported template 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 resourceTemplates = []
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 }`). Matching task
272
- // with the same id holds `run` (see ADR 0003).
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
- // Layouts wrap every page render. The bundle exports `id` and a default
348
- // export (the React component). Only one `*.layout.jsx` per app is
349
- // supported. They don't produce routable entries; they accumulate into
350
- // `manifest.layouts` so the server can inject them at render time.
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 layoutId = mod && mod.id
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 (typeof layoutId !== 'string' || layoutId.trim() === '') {
482
+ if (!layoutId) {
355
483
  this.error(
356
- `[@ossy/app][build] layout entry ${entryInfo.sourcePath} must export a non-empty string "id"`,
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
- // Resources are pure data — the default export *is* the template.
404
- // They don't use `metadata`, don't need a derived id, and don't
405
- // produce a routable manifest entry; they accumulate into a separate
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)) {
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] resource entry ${entryInfo.sourcePath} must default-export an object`,
566
+ `[@ossy/app][build] task entry ${entryInfo.sourcePath} must export a non-empty string "metadata.id"`,
412
567
  )
413
568
  }
414
- if (typeof template.id !== 'string' || template.id.trim() === '') {
569
+ if (isActionId(taskId)) {
415
570
  this.error(
416
- `[@ossy/app][build] resource entry ${entryInfo.sourcePath} default export is missing a non-empty string "id"`,
571
+ `[@ossy/app][build] Task "${taskId}" uses /actions/ use /tasks/ (e.g. "${taskIdFromActionId(taskId)}")`,
417
572
  )
418
573
  }
419
- if (seenResourceIds.has(template.id)) {
420
- this.error(`[@ossy/app][build] Duplicate resource template id "${template.id}"`)
574
+ if (seenIds.task.has(taskId)) {
575
+ this.error(`[@ossy/app][build] Duplicate task id "${taskId}"`)
421
576
  }
422
- seenResourceIds.add(template.id)
423
- resourceTemplates.push({ ...template, package: entryPackage(entryInfo, appPackageName) })
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 shell slots in `*.layout.jsx` via `export const slots`.',
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
- entries.push({
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
- entries.push({ type: 'api', id, path: rawMeta.path, entry: url, package: entryPackage(entryInfo, appPackageName) })
471
- } else {
472
- entries.push({ type: 'task', id, entry: url, package: entryPackage(entryInfo, appPackageName) })
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
- if (layouts.length > 1) {
477
- this.error(
478
- `[@ossy/app][build] Only one *.layout.jsx per app is allowed (found ${layouts.length}). ` +
479
- 'Use conditional rendering inside the app layout for route-specific chrome.',
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
- resourceTemplates,
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": "1.40.2",
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-shell-slots": "./runtime/resolve-shell-slots.js",
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/design-system": "^1.40.2",
46
- "@ossy/locale": "^1.40.2",
47
- "@ossy/pages": "^1.23.0",
48
- "@ossy/platform": "^1.39.2",
49
- "@ossy/router": "^1.40.2",
50
- "@ossy/router-react": "^1.40.2",
51
- "@ossy/sdk": "^1.40.2",
52
- "@ossy/sdk-react": "^1.40.2",
53
- "@ossy/themes": "^1.40.2",
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": "2b8745d57fee8b6c08787e2755b4df489291a5cd"
99
+ "gitHead": "4a70ec216448680d2fddc57b2a8a0077489720ae"
88
100
  }