@ossy/app 1.39.6 → 1.40.0

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 CHANGED
@@ -113,22 +113,25 @@ Build output: `build/.ossy/tasks.generated.json` — a registry of `[{ type, mod
113
113
 
114
114
  ## Components (`*.component.jsx`)
115
115
 
116
- Create `*.component.jsx` files in `src/` (or in any package dependency) to register slot-injectable components. The build discovers them automatically and bundles them separately — they are loaded on every page and slotted in by the layout when `metadata.id` matches the active page id.
116
+ Register injectable UI via `*.component.jsx` in `src/` or feature packages. Components are bundled separately and resolved into slots at runtime.
117
117
 
118
- ```jsx
119
- // src/workspace-users.component.jsx
120
- export const metadata = {
121
- id: 'workspace/users', // rendered when pageId === 'workspace/users'
122
- }
118
+ **Shell chrome** — the app assigns placement in `*.layout.jsx`:
123
119
 
124
- export default function WorkspaceUsersContent() {
125
- return <div>Users content</div>
120
+ ```jsx
121
+ export const slots = {
122
+ 'shell:header': 'app-header',
123
+ 'shell:sidebar': 'app-sidebar',
126
124
  }
127
125
  ```
128
126
 
129
- In your layout, use `<Slot name={app?.pageId} fallback={children} />` from `@ossy/design-system` to render the matching component (or fall back to the page's own output).
127
+ **Resource / input UI** feature packages use `metadata.id` as the slot key:
128
+
129
+ ```jsx
130
+ export const metadata = { id: 'resource:booking/service/form' }
131
+ export default function ServiceForm() { … }
132
+ ```
130
133
 
131
- See [`docs/component-primitive.md`](./docs/component-primitive.md) for the full contract, flow diagram, and examples.
134
+ See [`docs/component-primitive.md`](./docs/component-primitive.md) and [design-system/docs/SLOTS.md](../design-system/docs/SLOTS.md).
132
135
 
133
136
  ## Port configuration
134
137
 
package/cli/build.task.js CHANGED
@@ -22,10 +22,12 @@ import getPlatformFiles, {
22
22
  ACTION_FILE_PATTERN,
23
23
  E2E_FILE_PATTERN,
24
24
  LAYOUT_FILE_PATTERN,
25
+ TRANSLATIONS_FILE_PATTERN,
25
26
  } from './get-platform-files.task.js'
26
27
  import { manifestPlugin } from './manifest-plugin.js'
28
+ import { mergeAndEmitTranslations } from './merge-translations.task.js'
27
29
 
28
- export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, RESOURCE_FILE_PATTERN, COMPONENT_FILE_PATTERN, AGGREGATE_FILE_PATTERN, INTEGRATION_FILE_PATTERN, STARTUP_FILE_PATTERN, EMAIL_FILE_PATTERN, ACTION_FILE_PATTERN, E2E_FILE_PATTERN, LAYOUT_FILE_PATTERN }
30
+ export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, RESOURCE_FILE_PATTERN, COMPONENT_FILE_PATTERN, AGGREGATE_FILE_PATTERN, INTEGRATION_FILE_PATTERN, STARTUP_FILE_PATTERN, EMAIL_FILE_PATTERN, ACTION_FILE_PATTERN, E2E_FILE_PATTERN, LAYOUT_FILE_PATTERN, TRANSLATIONS_FILE_PATTERN }
29
31
 
30
32
  // Generated entry stubs live under `build/.ossy/entries/`. Putting them
31
33
  // inside `build/` means they're cleaned automatically with every build,
@@ -137,14 +139,8 @@ function generateResourceStub ({ stubAbs, sourceAbs }) {
137
139
  }
138
140
 
139
141
  // Components are re-exported as-is — the manifest plugin reads `metadata.id`
140
- // and the optional `slot` export from the bundled output. Components don't get
141
- // route stubs or hydration bootstrapping; they're loaded on-demand at runtime.
142
- // The `slot` export (when present) identifies the named shell slot this
143
- // component fills automatically — see PlatformShell for the slot registry.
144
- // We use `export *` for named exports rather than an explicit list because
145
- // `slot` is optional: some components don't export it, and a static named
146
- // re-export of a binding that doesn't exist causes a Rollup MISSING_EXPORT
147
- // error at build time.
142
+ // from the bundled output. Shell placement is app-controlled via `export const slots`
143
+ // in `*.layout.jsx`, not `export const slot` on components.
148
144
  function generateComponentStub ({ stubAbs, sourceAbs }) {
149
145
  const importPath = relImport(stubAbs, sourceAbs)
150
146
  return [
@@ -191,13 +187,13 @@ function generateStartupStub ({ stubAbs, sourceAbs }) {
191
187
  ].join('\n')
192
188
  }
193
189
 
194
- // Emails are React components. The stub re-exports `id`, `subject`, and the
195
- // default component export so the manifest plugin can validate them.
190
+ // Emails are React components. The stub re-exports `id` and the default component.
191
+ // Optional `subject` may be resolved from translations at send time instead.
196
192
  function generateEmailStub ({ stubAbs, sourceAbs }) {
197
193
  const importPath = relImport(stubAbs, sourceAbs)
198
194
  return [
199
195
  '// Generated by @ossy/app — do not edit',
200
- `export { id, subject, default } from '${importPath}'`,
196
+ `export { id, default } from '${importPath}'`,
201
197
  '',
202
198
  ].join('\n')
203
199
  }
@@ -261,6 +257,15 @@ async function loadConfig (configPath) {
261
257
  return mod.default || {}
262
258
  }
263
259
 
260
+ function readAppPackageName (cwd) {
261
+ try {
262
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
263
+ return typeof pkgJson.name === 'string' && pkgJson.name.trim() ? pkgJson.name : '@ossy/app'
264
+ } catch {
265
+ return '@ossy/app'
266
+ }
267
+ }
268
+
264
269
  export async function build (cliArgs = []) {
265
270
  arg(
266
271
  {
@@ -317,6 +322,7 @@ export async function build (cliArgs = []) {
317
322
 
318
323
  const configPath = path.resolve(srcDir, 'config.js')
319
324
  const configValue = await loadConfig(configPath)
325
+ const appPackageName = readAppPackageName(cwd)
320
326
 
321
327
  const middlewareSrc = path.resolve(srcDir, 'middleware.js')
322
328
  if (fs.existsSync(middlewareSrc)) {
@@ -374,7 +380,9 @@ export async function build (cliArgs = []) {
374
380
  srcDir,
375
381
  staticOutDir,
376
382
  configValue,
383
+ appPackageName,
377
384
  manifestPath: path.join(buildPath, 'manifest.json'),
385
+ projectRoot: cwd,
378
386
  }),
379
387
  ],
380
388
  })
@@ -393,4 +401,12 @@ export async function build (cliArgs = []) {
393
401
  fs.cpSync(publicSrc, publicOutDir, { recursive: true, force: true })
394
402
  }
395
403
 
404
+ mergeAndEmitTranslations({
405
+ srcDir,
406
+ projectRoot: cwd,
407
+ publicOutDir,
408
+ manifestPath: path.join(buildPath, 'manifest.json'),
409
+ configValue,
410
+ warn: (message) => console.warn(message),
411
+ })
396
412
  }
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ import {
5
+ discoverFilesByPattern,
6
+ TRANSLATIONS_FILE_PATTERN,
7
+ } from './get-platform-files.task.js'
8
+ import {
9
+ isValidLocaleCode,
10
+ parseLocaleFromTranslationFilename,
11
+ } from '@ossy/locale'
12
+
13
+ /**
14
+ * @param {string} projectRoot
15
+ * @returns {string[]}
16
+ */
17
+ function collectNodeModulesDirs (projectRoot) {
18
+ const dirs = []
19
+ let current = projectRoot
20
+ while (true) {
21
+ const nm = path.join(current, 'node_modules')
22
+ if (fs.existsSync(nm) && fs.statSync(nm).isDirectory()) dirs.push(nm)
23
+ const parent = path.dirname(current)
24
+ if (parent === current) break
25
+ current = parent
26
+ }
27
+ return dirs
28
+ }
29
+
30
+ /**
31
+ * @typedef {import('@ossy/locale').TranslationSourceFile} TranslationSourceFile
32
+ */
33
+
34
+ /**
35
+ * Discover `*.translations.json` from installed packages and app `src/`.
36
+ *
37
+ * @param {{ srcDir: string, projectRoot: string }} options
38
+ * @returns {TranslationSourceFile[]}
39
+ */
40
+ export function discoverTranslationFiles ({ srcDir, projectRoot }) {
41
+ /** @type {TranslationSourceFile[]} */
42
+ const files = []
43
+ const seenPaths = new Set()
44
+
45
+ const nmDirs = collectNodeModulesDirs(projectRoot)
46
+ /** @type {Array<{ packageName: string, sourcePath: string }>} */
47
+ const packageFiles = []
48
+
49
+ const tryPackageDir = (pkgDir) => {
50
+ const real = fs.realpathSync(pkgDir)
51
+ const pkgJsonPath = path.join(real, 'package.json')
52
+ if (!fs.existsSync(pkgJsonPath)) return
53
+
54
+ let pkg
55
+ try {
56
+ pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
57
+ } catch {
58
+ return
59
+ }
60
+ if (!pkg.ossy?.src) return
61
+
62
+ const packageSrcDir = path.resolve(real, pkg.ossy.src)
63
+ const matches = discoverFilesByPattern(packageSrcDir, TRANSLATIONS_FILE_PATTERN)
64
+ for (const sourcePath of matches) {
65
+ const stat = fs.statSync(sourcePath)
66
+ if (!stat.isFile() || stat.size === 0) continue
67
+ packageFiles.push({ packageName: pkg.name, sourcePath })
68
+ }
69
+ }
70
+
71
+ for (const nmDir of nmDirs) {
72
+ for (const entry of fs.readdirSync(nmDir, { withFileTypes: true })) {
73
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
74
+ if (entry.name.startsWith('@')) {
75
+ const scopeDir = path.join(nmDir, entry.name)
76
+ if (!fs.existsSync(scopeDir)) continue
77
+ for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
78
+ if (scoped.isDirectory() || scoped.isSymbolicLink()) {
79
+ tryPackageDir(path.join(scopeDir, scoped.name))
80
+ }
81
+ }
82
+ } else {
83
+ tryPackageDir(path.join(nmDir, entry.name))
84
+ }
85
+ }
86
+ }
87
+
88
+ packageFiles.sort((a, b) => {
89
+ const byPackage = a.packageName.localeCompare(b.packageName)
90
+ if (byPackage !== 0) return byPackage
91
+ return a.sourcePath.localeCompare(b.sourcePath)
92
+ })
93
+
94
+ for (const { packageName, sourcePath } of packageFiles) {
95
+ if (seenPaths.has(sourcePath)) continue
96
+ seenPaths.add(sourcePath)
97
+ const locale = parseLocaleFromTranslationFilename(path.basename(sourcePath))
98
+ if (!locale) continue
99
+ files.push({
100
+ locale,
101
+ sourcePath,
102
+ tier: 'package',
103
+ packageName,
104
+ sortKey: `${packageName}:${sourcePath}`,
105
+ })
106
+ }
107
+
108
+ const appFiles = discoverFilesByPattern(srcDir, TRANSLATIONS_FILE_PATTERN).sort()
109
+ for (const sourcePath of appFiles) {
110
+ if (seenPaths.has(sourcePath)) continue
111
+ seenPaths.add(sourcePath)
112
+ const stat = fs.statSync(sourcePath)
113
+ if (!stat.isFile() || stat.size === 0) continue
114
+ const locale = parseLocaleFromTranslationFilename(path.basename(sourcePath))
115
+ if (!locale) continue
116
+ files.push({
117
+ locale,
118
+ sourcePath,
119
+ tier: 'app',
120
+ sortKey: sourcePath,
121
+ })
122
+ }
123
+
124
+ return files
125
+ }
126
+
127
+ /**
128
+ * @param {TranslationSourceFile[]} files
129
+ */
130
+ export function validateTranslationLocales (files) {
131
+ for (const file of files) {
132
+ if (!isValidLocaleCode(file.locale)) {
133
+ throw new Error(
134
+ `[@ossy/app][build] Invalid locale "${file.locale}" in ${file.sourcePath}. ` +
135
+ 'Expected ISO 639-1 code (e.g. en, sv).',
136
+ )
137
+ }
138
+ }
139
+ }
@@ -13,6 +13,7 @@ export const EMAIL_FILE_PATTERN = /\.email\.(jsx?|tsx?)$/
13
13
  export const ACTION_FILE_PATTERN = /\.action\.(mjs|cjs|js)$/
14
14
  export const E2E_FILE_PATTERN = /\.e2e\.(mjs|cjs|js)$/
15
15
  export const LAYOUT_FILE_PATTERN = /\.layout\.(jsx?|tsx?)$/
16
+ export const TRANSLATIONS_FILE_PATTERN = /\.translations\.json$/
16
17
 
17
18
  /**
18
19
  * @typedef {'page' | 'api' | 'task' | 'resource' | 'component' | 'aggregate' | 'integration' | 'startup' | 'email' | 'action' | 'e2e' | 'layout'} EntryKind
@@ -3,6 +3,7 @@ import path from 'node:path'
3
3
  import { pathToFileURL } from 'node:url'
4
4
 
5
5
  import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.js'
6
+ import { discoverPackageDefinitions } from '../src/manifest/discover-package-definitions.js'
6
7
 
7
8
  /**
8
9
  * @typedef {import('./get-platform-files.task.js').PlatformEntry} PlatformEntry
@@ -24,8 +25,13 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
24
25
  * The serializable config object (typically `src/config.js`'s default
25
26
  * export). Inlined under `manifest.config` so the platform server doesn't
26
27
  * need a runtime `import('./config.js')`.
28
+ * @property {string} appPackageName
29
+ * npm package name of the app being built (e.g. `@ossy/app-test`). Used as
30
+ * the `package` field on manifest entries that live in the app `src/` tree.
27
31
  * @property {string} manifestPath
28
32
  * Absolute path to write the manifest to (typically `build/manifest.json`).
33
+ * @property {string} projectRoot
34
+ * Absolute path to the consuming app root — used to discover package definitions.
29
35
  *
30
36
  * @typedef {'page' | 'api' | 'task'} ManifestEntryType
31
37
  *
@@ -41,12 +47,6 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
41
47
  * @typedef {object} ComponentManifestEntry
42
48
  * @property {string} id Unique component id (from `metadata.id`).
43
49
  * @property {string} entry URL the platform serves the component bundle from.
44
- * @property {string} [slot] Named shell slot this component fills (from `export const slot`).
45
- *
46
- * @typedef {object} SlotManifestEntry
47
- * @property {string} slot Named shell slot (e.g. `'header'`, `'sidebar'`, `'resource:booking/service/form'`).
48
- * @property {string} entry URL the platform serves the component bundle from.
49
- * @property {string} [package] npm package name that contributed this slot (from installed packages).
50
50
  *
51
51
  * @typedef {object} AggregateManifestEntry
52
52
  * @property {string} id AggregateType string (e.g. `'User'`).
@@ -73,6 +73,8 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
73
73
  * @typedef {object} LayoutManifestEntry
74
74
  * @property {string} id Unique layout id (e.g. `'app-shell'`).
75
75
  * @property {string} entry URL the platform serves the layout bundle from.
76
+ * @property {Record<string, string>} [slots]
77
+ * App-controlled slot map (`slotName` → component `metadata.id`), from `export const slots` in `*.layout.jsx`.
76
78
  *
77
79
  * @typedef {object} E2eManifestEntry
78
80
  * @property {string} id Unique test id (e.g. `'authentication/sign-in'`).
@@ -83,7 +85,6 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
83
85
  * @typedef {object} Manifest
84
86
  * @property {ManifestEntry[]} entries Flat, ordered list of every routable thing.
85
87
  * @property {ComponentManifestEntry[]} components Discovered `*.component.*` entries, keyed by id.
86
- * @property {SlotManifestEntry[]} slots Components that declare `export const slot` — auto-injected into shell slots.
87
88
  * @property {object[]} resourceTemplates Each `*.resource.js` file's default-exported template object.
88
89
  * @property {AggregateManifestEntry[]} aggregates Discovered `*.aggregate.js` entries from installed packages.
89
90
  * @property {IntegrationManifestEntry[]} integrations Discovered `*.integration.js` entries.
@@ -91,8 +92,9 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
91
92
  * @property {EmailManifestEntry[]} emails Discovered `*.email.{jsx,tsx}` entries.
92
93
  * @property {ActionManifestEntry[]} actions Discovered `*.action.js` entries.
93
94
  * @property {E2eManifestEntry[]} e2es Discovered `*.e2e.js` entries from installed packages.
94
- * @property {LayoutManifestEntry[]} layouts Discovered `*.layout.{jsx,tsx}` entries.
95
+ * @property {LayoutManifestEntry[]} layouts Discovered `*.layout.{jsx,tsx}` entries (includes app `slots` map).
95
96
  * @property {object} config Inlined `src/config.js` default export.
97
+ * @property {Record<string, object>} [definitions] Package Definition metadata keyed by slug.
96
98
  */
97
99
 
98
100
  /**
@@ -129,7 +131,11 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
129
131
  * @param {ManifestPluginOptions} options
130
132
  * @returns {import('rollup').Plugin}
131
133
  */
132
- export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configValue, manifestPath }) {
134
+ function entryPackage (entryInfo, appPackageName) {
135
+ return entryInfo.packageName || appPackageName
136
+ }
137
+
138
+ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configValue, appPackageName = '@ossy/app', manifestPath, projectRoot }) {
133
139
  return {
134
140
  name: 'ossy-manifest',
135
141
  async writeBundle (_, bundle) {
@@ -156,8 +162,6 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
156
162
  const seenStartupIds = new Set()
157
163
  const seenEmailIds = new Set()
158
164
  const seenActionIds = new Set()
159
- /** @type {SlotManifestEntry[]} */
160
- const slots = []
161
165
  /** @type {E2eManifestEntry[]} */
162
166
  const e2es = []
163
167
  const seenE2eIds = new Set()
@@ -213,7 +217,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
213
217
  this.error(`[@ossy/app][build] Duplicate integration id "${integrationId}"`)
214
218
  }
215
219
  seenIntegrationIds.add(integrationId)
216
- integrations.push({ id: integrationId, entry: url, credentials })
220
+ integrations.push({ id: integrationId, entry: url, credentials, package: entryPackage(entryInfo, appPackageName) })
217
221
  continue
218
222
  }
219
223
 
@@ -236,7 +240,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
236
240
  this.error(`[@ossy/app][build] Duplicate startup id "${startupId}"`)
237
241
  }
238
242
  seenStartupIds.add(startupId)
239
- startups.push({ id: startupId, entry: url })
243
+ startups.push({ id: startupId, entry: url, package: entryPackage(entryInfo, appPackageName) })
240
244
  continue
241
245
  }
242
246
 
@@ -260,7 +264,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
260
264
  this.error(`[@ossy/app][build] Duplicate email id "${emailId}"`)
261
265
  }
262
266
  seenEmailIds.add(emailId)
263
- emails.push({ id: emailId, entry: url })
267
+ emails.push({ id: emailId, entry: url, package: entryPackage(entryInfo, appPackageName) })
264
268
  continue
265
269
  }
266
270
 
@@ -284,7 +288,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
284
288
  }
285
289
  const access = mod.access ?? 'authenticated'
286
290
  seenActionIds.add(actionId)
287
- actions.push({ id: actionId, entry: url, access })
291
+ actions.push({ id: actionId, entry: url, access, package: entryPackage(entryInfo, appPackageName) })
288
292
  continue
289
293
  }
290
294
 
@@ -308,7 +312,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
308
312
  this.error(`[@ossy/app][build] Duplicate aggregate id "${aggregateId}"`)
309
313
  }
310
314
  seenAggregateIds.add(aggregateId)
311
- aggregates.push({ id: aggregateId, entry: url })
315
+ aggregates.push({ id: aggregateId, entry: url, package: entryPackage(entryInfo, appPackageName) })
312
316
  continue
313
317
  }
314
318
 
@@ -341,6 +345,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
341
345
  feature: typeof feature === 'string' ? feature : '',
342
346
  requires: Array.isArray(requires) ? requires : [],
343
347
  entry: url,
348
+ package: entryPackage(entryInfo, appPackageName),
344
349
  })
345
350
  continue
346
351
  }
@@ -366,7 +371,38 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
366
371
  this.error(`[@ossy/app][build] Duplicate layout id "${layoutId}"`)
367
372
  }
368
373
  seenLayoutIds.add(layoutId)
369
- layouts.push({ id: layoutId, entry: url })
374
+ /** @type {Record<string, string> | undefined} */
375
+ let layoutSlots
376
+ const rawSlots = mod.slots
377
+ if (rawSlots != null) {
378
+ if (typeof rawSlots !== 'object' || Array.isArray(rawSlots)) {
379
+ this.error(
380
+ `[@ossy/app][build] layout entry ${entryInfo.sourcePath} must export \`slots\` as a plain object (slot name → component id)`,
381
+ )
382
+ }
383
+ layoutSlots = {}
384
+ for (const [slotKey, componentId] of Object.entries(rawSlots)) {
385
+ if (typeof slotKey !== 'string' || typeof componentId !== 'string') {
386
+ this.error(
387
+ `[@ossy/app][build] layout slots in ${entryInfo.sourcePath} must map string slot names to string component ids`,
388
+ )
389
+ }
390
+ const sk = slotKey.trim()
391
+ const cid = componentId.trim()
392
+ if (!sk || !cid) {
393
+ this.error(
394
+ `[@ossy/app][build] layout slots in ${entryInfo.sourcePath} must use non-empty slot names and component ids`,
395
+ )
396
+ }
397
+ layoutSlots[sk] = cid
398
+ }
399
+ }
400
+ layouts.push({
401
+ id: layoutId,
402
+ entry: url,
403
+ package: entryPackage(entryInfo, appPackageName),
404
+ ...(layoutSlots && Object.keys(layoutSlots).length > 0 ? { slots: layoutSlots } : {}),
405
+ })
370
406
  continue
371
407
  }
372
408
 
@@ -390,7 +426,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
390
426
  this.error(`[@ossy/app][build] Duplicate resource template id "${template.id}"`)
391
427
  }
392
428
  seenResourceIds.add(template.id)
393
- resourceTemplates.push(template)
429
+ resourceTemplates.push({ ...template, package: entryPackage(entryInfo, appPackageName) })
394
430
  continue
395
431
  }
396
432
 
@@ -405,12 +441,13 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
405
441
  seenIds[entryInfo.kind].add(id)
406
442
 
407
443
  if (entryInfo.kind === 'component') {
408
- const slotName = typeof mod.slot === 'string' && mod.slot.trim() ? mod.slot.trim() : undefined
409
- components.push({ id, entry: url, ...(slotName ? { slot: slotName } : {}) })
410
- if (slotName) {
411
- const packageName = entryInfo.packageName
412
- slots.push({ slot: slotName, entry: url, ...(packageName ? { package: packageName } : {}) })
444
+ if (mod.slot != null) {
445
+ this.error(
446
+ `[@ossy/app][build] component ${entryInfo.sourcePath} must not export \`slot\`. ` +
447
+ 'Assign components to shell slots in `*.layout.jsx` via `export const slots`.',
448
+ )
413
449
  }
450
+ components.push({ id, entry: url, package: entryPackage(entryInfo, appPackageName) })
414
451
  continue
415
452
  }
416
453
 
@@ -422,6 +459,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
422
459
  path: pagePath,
423
460
  title: rawMeta.title,
424
461
  entry: url,
462
+ package: entryPackage(entryInfo, appPackageName),
425
463
  })
426
464
  } else if (entryInfo.kind === 'api') {
427
465
  // APIs have no sensible default route (unlike pages where we can
@@ -435,9 +473,9 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
435
473
  `Add a \`path\` to the metadata or default-export object (e.g. \`path: '/api/${id}'\`).`,
436
474
  )
437
475
  }
438
- entries.push({ type: 'api', id, path: rawMeta.path, entry: url })
476
+ entries.push({ type: 'api', id, path: rawMeta.path, entry: url, package: entryPackage(entryInfo, appPackageName) })
439
477
  } else {
440
- entries.push({ type: 'task', id, entry: url })
478
+ entries.push({ type: 'task', id, entry: url, package: entryPackage(entryInfo, appPackageName) })
441
479
  }
442
480
  }
443
481
 
@@ -449,7 +487,24 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
449
487
  }
450
488
 
451
489
  /** @type {Manifest} */
452
- const manifest = { entries, components, slots, resourceTemplates, aggregates, integrations, startups, emails, actions, e2es, layouts, config: configValue || {} }
490
+ const definitions = projectRoot
491
+ ? await discoverPackageDefinitions(projectRoot)
492
+ : {}
493
+
494
+ const manifest = {
495
+ entries,
496
+ components,
497
+ resourceTemplates,
498
+ aggregates,
499
+ integrations,
500
+ startups,
501
+ emails,
502
+ actions,
503
+ e2es,
504
+ layouts,
505
+ definitions,
506
+ config: configValue || {},
507
+ }
453
508
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
454
509
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
455
510
  },