@ossy/app 1.34.0 → 1.35.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/cli/build.task.js CHANGED
@@ -18,10 +18,12 @@ import getPlatformFiles, {
18
18
  AGGREGATE_FILE_PATTERN,
19
19
  INTEGRATION_FILE_PATTERN,
20
20
  STARTUP_FILE_PATTERN,
21
+ EMAIL_FILE_PATTERN,
22
+ ACTION_FILE_PATTERN,
21
23
  } from './get-platform-files.task.js'
22
24
  import { manifestPlugin } from './manifest-plugin.js'
23
25
 
24
- 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 }
26
+ 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 }
25
27
 
26
28
  // Generated entry stubs live under `build/.ossy/entries/`. Putting them
27
29
  // inside `build/` means they're cleaned automatically with every build,
@@ -71,7 +73,7 @@ function entryNameFromSource (sourcePath, srcDir, packageName) {
71
73
  }
72
74
 
73
75
  function stubFileNameFor (kind, sourcePath, srcDir, packageName) {
74
- const ext = (kind === 'page' || kind === 'component') ? '.entry.jsx' : '.entry.js'
76
+ const ext = (kind === 'page' || kind === 'component' || kind === 'email') ? '.entry.jsx' : '.entry.js'
75
77
  return entryNameFromSource(sourcePath, srcDir, packageName) + ext
76
78
  }
77
79
 
@@ -182,6 +184,29 @@ function generateStartupStub ({ stubAbs, sourceAbs }) {
182
184
  ].join('\n')
183
185
  }
184
186
 
187
+ // Emails are React components. The stub re-exports `id`, `subject`, and the
188
+ // default component export so the manifest plugin can validate them.
189
+ function generateEmailStub ({ stubAbs, sourceAbs }) {
190
+ const importPath = relImport(stubAbs, sourceAbs)
191
+ return [
192
+ '// Generated by @ossy/app — do not edit',
193
+ `export { id, subject, default } from '${importPath}'`,
194
+ '',
195
+ ].join('\n')
196
+ }
197
+
198
+ // Actions are named command handlers. The stub re-exports `id`, `access`, and
199
+ // `run` so the manifest plugin can validate them and the platform server can
200
+ // invoke them at runtime.
201
+ function generateActionStub ({ stubAbs, sourceAbs }) {
202
+ const importPath = relImport(stubAbs, sourceAbs)
203
+ return [
204
+ '// Generated by @ossy/app — do not edit',
205
+ `export { id, access, run } from '${importPath}'`,
206
+ '',
207
+ ].join('\n')
208
+ }
209
+
185
210
  function stubFor (kind, args) {
186
211
  if (kind === 'page') return generatePageStub(args)
187
212
  if (kind === 'api') return generateApiStub(args)
@@ -190,6 +215,8 @@ function stubFor (kind, args) {
190
215
  if (kind === 'aggregate') return generateAggregateStub(args)
191
216
  if (kind === 'integration') return generateIntegrationStub(args)
192
217
  if (kind === 'startup') return generateStartupStub(args)
218
+ if (kind === 'email') return generateEmailStub(args)
219
+ if (kind === 'action') return generateActionStub(args)
193
220
  return generateTaskStub(args)
194
221
  }
195
222
 
@@ -234,6 +261,8 @@ export async function build (cliArgs = []) {
234
261
  ...platformFiles.aggregates,
235
262
  ...platformFiles.integrations,
236
263
  ...platformFiles.startups,
264
+ ...platformFiles.emails,
265
+ ...platformFiles.actions,
237
266
  ]
238
267
 
239
268
  const entriesByStub = new Map()
@@ -9,9 +9,11 @@ export const COMPONENT_FILE_PATTERN = /\.component\.(jsx?|tsx?)$/
9
9
  export const AGGREGATE_FILE_PATTERN = /\.aggregate\.(mjs|cjs|js)$/
10
10
  export const INTEGRATION_FILE_PATTERN = /\.integration\.(mjs|cjs|js)$/
11
11
  export const STARTUP_FILE_PATTERN = /\.startup\.(mjs|cjs|js)$/
12
+ export const EMAIL_FILE_PATTERN = /\.email\.(jsx?|tsx?)$/
13
+ export const ACTION_FILE_PATTERN = /\.action\.(mjs|cjs|js)$/
12
14
 
13
15
  /**
14
- * @typedef {'page' | 'api' | 'task' | 'resource' | 'component' | 'aggregate' | 'integration' | 'startup'} EntryKind
16
+ * @typedef {'page' | 'api' | 'task' | 'resource' | 'component' | 'aggregate' | 'integration' | 'startup' | 'email' | 'action'} EntryKind
15
17
  *
16
18
  * @typedef {object} PlatformEntry
17
19
  * @property {EntryKind} kind Which bucket this entry lives in.
@@ -28,6 +30,8 @@ export const STARTUP_FILE_PATTERN = /\.startup\.(mjs|cjs|js)$/
28
30
  * @property {PlatformEntry[]} aggregates Discovered `*.aggregate.{js,mjs,cjs}` entries (installed packages only).
29
31
  * @property {PlatformEntry[]} integrations Discovered `*.integration.{js,mjs,cjs}` entries.
30
32
  * @property {PlatformEntry[]} startups Discovered `*.startup.{js,mjs,cjs}` entries.
33
+ * @property {PlatformEntry[]} emails Discovered `*.email.{jsx,tsx,...}` entries.
34
+ * @property {PlatformEntry[]} actions Discovered `*.action.{js,mjs,cjs}` entries.
31
35
  */
32
36
  export function discoverFilesByPattern (srcDir, filePattern) {
33
37
  const dir = path.resolve(srcDir)
@@ -54,6 +58,8 @@ function classifyFile (absPath) {
54
58
  if (AGGREGATE_FILE_PATTERN.test(base)) return 'aggregate'
55
59
  if (INTEGRATION_FILE_PATTERN.test(base)) return 'integration'
56
60
  if (STARTUP_FILE_PATTERN.test(base)) return 'startup'
61
+ if (EMAIL_FILE_PATTERN.test(base)) return 'email'
62
+ if (ACTION_FILE_PATTERN.test(base)) return 'action'
57
63
  return null
58
64
  }
59
65
 
@@ -126,6 +132,8 @@ export function discoverInstalledPackageEntries (projectRoot) {
126
132
  ...discoverFilesByPattern(packageSrcDir, AGGREGATE_FILE_PATTERN),
127
133
  ...discoverFilesByPattern(packageSrcDir, INTEGRATION_FILE_PATTERN),
128
134
  ...discoverFilesByPattern(packageSrcDir, STARTUP_FILE_PATTERN),
135
+ ...discoverFilesByPattern(packageSrcDir, EMAIL_FILE_PATTERN),
136
+ ...discoverFilesByPattern(packageSrcDir, ACTION_FILE_PATTERN),
129
137
  ]
130
138
  for (const sourcePath of files) {
131
139
  const stat = fs.statSync(sourcePath)
@@ -169,8 +177,8 @@ export function discoverInstalledPackageEntries (projectRoot) {
169
177
  * @returns {Promise<PlatformFiles>}
170
178
  */
171
179
  export default async function getPlatformFiles (srcDir) {
172
- const out = { pages: [], apis: [], tasks: [], resources: [], components: [], aggregates: [], integrations: [], startups: [] }
173
- const bucketByKind = { page: 'pages', api: 'apis', task: 'tasks', resource: 'resources', component: 'components', aggregate: 'aggregates', integration: 'integrations', startup: 'startups' }
180
+ const out = { pages: [], apis: [], tasks: [], resources: [], components: [], aggregates: [], integrations: [], startups: [], emails: [], actions: [] }
181
+ const bucketByKind = { page: 'pages', api: 'apis', task: 'tasks', resource: 'resources', component: 'components', aggregate: 'aggregates', integration: 'integrations', startup: 'startups', email: 'emails', action: 'actions' }
174
182
 
175
183
  // Aggregates are only discovered from installed packages — never from the
176
184
  // local `src/` — to avoid pulling in per-project duplicates of the same
@@ -183,6 +191,8 @@ export default async function getPlatformFiles (srcDir) {
183
191
  ...discoverFilesByPattern(srcDir, COMPONENT_FILE_PATTERN),
184
192
  ...discoverFilesByPattern(srcDir, INTEGRATION_FILE_PATTERN),
185
193
  ...discoverFilesByPattern(srcDir, STARTUP_FILE_PATTERN),
194
+ ...discoverFilesByPattern(srcDir, EMAIL_FILE_PATTERN),
195
+ ...discoverFilesByPattern(srcDir, ACTION_FILE_PATTERN),
186
196
  ]
187
197
  for (const sourcePath of localFiles) {
188
198
  const stat = fs.statSync(sourcePath)
@@ -55,6 +55,15 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
55
55
  * @property {string} id Unique startup slug (e.g. `'create-bot-user'`).
56
56
  * @property {string} entry URL the platform serves the startup bundle from.
57
57
  *
58
+ * @typedef {object} EmailManifestEntry
59
+ * @property {string} id Unique email id (e.g. `'authentication/verify-sign-in'`).
60
+ * @property {string} entry URL the platform serves the email bundle from.
61
+ *
62
+ * @typedef {object} ActionManifestEntry
63
+ * @property {string} id Unique action slug (e.g. `'authentication/request-sign-in'`).
64
+ * @property {string} entry URL the platform serves the action bundle from.
65
+ * @property {string} access Access level: `'public'` | `'authenticated'` | `'workspace'`.
66
+ *
58
67
  * @typedef {object} Manifest
59
68
  * @property {ManifestEntry[]} entries Flat, ordered list of every routable thing.
60
69
  * @property {ComponentManifestEntry[]} components Discovered `*.component.*` entries, keyed by id.
@@ -62,6 +71,8 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
62
71
  * @property {AggregateManifestEntry[]} aggregates Discovered `*.aggregate.js` entries from installed packages.
63
72
  * @property {IntegrationManifestEntry[]} integrations Discovered `*.integration.js` entries.
64
73
  * @property {StartupManifestEntry[]} startups Discovered `*.startup.js` entries.
74
+ * @property {EmailManifestEntry[]} emails Discovered `*.email.{jsx,tsx}` entries.
75
+ * @property {ActionManifestEntry[]} actions Discovered `*.action.js` entries.
65
76
  * @property {object} config Inlined `src/config.js` default export.
66
77
  */
67
78
 
@@ -115,11 +126,17 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
115
126
  const integrations = []
116
127
  /** @type {StartupManifestEntry[]} */
117
128
  const startups = []
129
+ /** @type {EmailManifestEntry[]} */
130
+ const emails = []
131
+ /** @type {ActionManifestEntry[]} */
132
+ const actions = []
118
133
  const seenIds = { page: new Set(), api: new Set(), task: new Set(), component: new Set() }
119
134
  const seenResourceIds = new Set()
120
135
  const seenAggregateIds = new Set()
121
136
  const seenIntegrationIds = new Set()
122
137
  const seenStartupIds = new Set()
138
+ const seenEmailIds = new Set()
139
+ const seenActionIds = new Set()
123
140
 
124
141
  for (const fileName of Object.keys(bundle)) {
125
142
  const chunk = bundle[fileName]
@@ -197,6 +214,54 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
197
214
  continue
198
215
  }
199
216
 
217
+ // Emails are React components that render to HTML/text via EmailRenderer.
218
+ // The bundle exports `id` (a namespaced slug) and a default export
219
+ // (the component). They don't produce routable entries.
220
+ if (entryInfo.kind === 'email') {
221
+ const emailId = mod && mod.id
222
+ const component = mod && mod.default
223
+ if (typeof emailId !== 'string' || emailId.trim() === '') {
224
+ this.error(
225
+ `[@ossy/app][build] email entry ${entryInfo.sourcePath} must export a non-empty string "id"`,
226
+ )
227
+ }
228
+ if (typeof component !== 'function') {
229
+ this.error(
230
+ `[@ossy/app][build] email entry ${entryInfo.sourcePath} must default-export a component function`,
231
+ )
232
+ }
233
+ if (seenEmailIds.has(emailId)) {
234
+ this.error(`[@ossy/app][build] Duplicate email id "${emailId}"`)
235
+ }
236
+ seenEmailIds.add(emailId)
237
+ emails.push({ id: emailId, entry: url })
238
+ continue
239
+ }
240
+
241
+ // Actions are named, discoverable command handlers. The bundle exports
242
+ // `id` (a unique slug), `run` (the handler function), and optionally
243
+ // `access` ('public' | 'authenticated' | 'workspace', default 'authenticated').
244
+ if (entryInfo.kind === 'action') {
245
+ const actionId = mod && mod.id
246
+ if (typeof actionId !== 'string' || actionId.trim() === '') {
247
+ this.error(
248
+ `[@ossy/app][build] action entry ${entryInfo.sourcePath} must export a non-empty string "id"`,
249
+ )
250
+ }
251
+ if (typeof mod.run !== 'function') {
252
+ this.error(
253
+ `[@ossy/app][build] action entry ${entryInfo.sourcePath} must export a "run" function`,
254
+ )
255
+ }
256
+ if (seenActionIds.has(actionId)) {
257
+ this.error(`[@ossy/app][build] Duplicate action id "${actionId}"`)
258
+ }
259
+ const access = mod.access ?? 'authenticated'
260
+ seenActionIds.add(actionId)
261
+ actions.push({ id: actionId, entry: url, access })
262
+ continue
263
+ }
264
+
200
265
  // Aggregates are server-side classes — the bundle exports `Aggregate`
201
266
  // (the class) and `id` (the AggregateType string). They don't produce
202
267
  // routable entries; they accumulate into a separate `aggregates` array.
@@ -282,7 +347,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
282
347
  }
283
348
 
284
349
  /** @type {Manifest} */
285
- const manifest = { entries, components, resourceTemplates, aggregates, integrations, startups, config: configValue || {} }
350
+ const manifest = { entries, components, resourceTemplates, aggregates, integrations, startups, emails, actions, config: configValue || {} }
286
351
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
287
352
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
288
353
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "description": "",
5
5
  "source": "./src/index.js",
6
6
  "main": "./src/index.js",
@@ -38,14 +38,14 @@
38
38
  "@babel/eslint-parser": "^7.15.8",
39
39
  "@babel/preset-react": "^7.26.3",
40
40
  "@babel/register": "^7.25.9",
41
- "@ossy/design-system": "^1.34.0",
41
+ "@ossy/design-system": "^1.35.0",
42
42
  "@ossy/pages": "^1.23.0",
43
- "@ossy/platform": "^1.33.0",
44
- "@ossy/router": "^1.34.0",
45
- "@ossy/router-react": "^1.34.0",
46
- "@ossy/sdk": "^1.34.0",
47
- "@ossy/sdk-react": "^1.34.0",
48
- "@ossy/themes": "^1.34.0",
43
+ "@ossy/platform": "^1.34.0",
44
+ "@ossy/router": "^1.35.0",
45
+ "@ossy/router-react": "^1.35.0",
46
+ "@ossy/sdk": "^1.35.0",
47
+ "@ossy/sdk-react": "^1.35.0",
48
+ "@ossy/themes": "^1.35.0",
49
49
  "@rollup/plugin-alias": "^6.0.0",
50
50
  "@rollup/plugin-babel": "^7.0.0",
51
51
  "@rollup/plugin-commonjs": "^29.0.0",
@@ -79,5 +79,5 @@
79
79
  "README.md",
80
80
  "tsconfig.json"
81
81
  ],
82
- "gitHead": "46cdd391ec01ea9210543ee7b14661f77079a7e7"
82
+ "gitHead": "c6697078268867d88553ca0bac08faad5cea1546"
83
83
  }