@ossy/app 1.36.0 → 1.37.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
@@ -20,10 +20,11 @@ import getPlatformFiles, {
20
20
  STARTUP_FILE_PATTERN,
21
21
  EMAIL_FILE_PATTERN,
22
22
  ACTION_FILE_PATTERN,
23
+ E2E_FILE_PATTERN,
23
24
  } from './get-platform-files.task.js'
24
25
  import { manifestPlugin } from './manifest-plugin.js'
25
26
 
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 }
27
+ 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 }
27
28
 
28
29
  // Generated entry stubs live under `build/.ossy/entries/`. Putting them
29
30
  // inside `build/` means they're cleaned automatically with every build,
@@ -117,9 +118,7 @@ function generateTaskStub ({ stubAbs, sourceAbs }) {
117
118
  const importPath = relImport(stubAbs, sourceAbs)
118
119
  return [
119
120
  '// Generated by @ossy/app — do not edit',
120
- `import * as _task from '${importPath}'`,
121
- 'export const metadata = _task.metadata',
122
- 'export const run = _task.run ?? _task.default',
121
+ `export * from '${importPath}'`,
123
122
  '',
124
123
  ].join('\n')
125
124
  }
@@ -207,6 +206,18 @@ function generateActionStub ({ stubAbs, sourceAbs }) {
207
206
  ].join('\n')
208
207
  }
209
208
 
209
+ // E2e tests are Playwright test modules. The stub re-exports `id`, `feature`,
210
+ // `requires`, and the default test function so the manifest plugin can validate
211
+ // them and the e2e runner can import and execute them.
212
+ function generateE2eStub ({ stubAbs, sourceAbs }) {
213
+ const importPath = relImport(stubAbs, sourceAbs)
214
+ return [
215
+ '// Generated by @ossy/app — do not edit',
216
+ `export { id, feature, requires, default } from '${importPath}'`,
217
+ '',
218
+ ].join('\n')
219
+ }
220
+
210
221
  function stubFor (kind, args) {
211
222
  if (kind === 'page') return generatePageStub(args)
212
223
  if (kind === 'api') return generateApiStub(args)
@@ -217,6 +228,7 @@ function stubFor (kind, args) {
217
228
  if (kind === 'startup') return generateStartupStub(args)
218
229
  if (kind === 'email') return generateEmailStub(args)
219
230
  if (kind === 'action') return generateActionStub(args)
231
+ if (kind === 'e2e') return generateE2eStub(args)
220
232
  return generateTaskStub(args)
221
233
  }
222
234
 
@@ -263,6 +275,7 @@ export async function build (cliArgs = []) {
263
275
  ...platformFiles.startups,
264
276
  ...platformFiles.emails,
265
277
  ...platformFiles.actions,
278
+ ...platformFiles.e2es,
266
279
  ]
267
280
 
268
281
  const entriesByStub = new Map()
@@ -297,7 +310,20 @@ export async function build (cliArgs = []) {
297
310
  // bundled task files at build time — bundling sharp would trigger its binary
298
311
  // loader during the build, which fails on linux-arm64 builders that don't
299
312
  // have the darwin binaries installed.
300
- external: (id) => id.startsWith('node:') || NODE_BUILTINS.has(id) || id === 'sharp' || id.startsWith('@img/sharp-'),
313
+ // Singleton packages must resolve to the already-loaded module in the
314
+ // Node.js module cache at runtime, not to a bundled copy with its own
315
+ // empty Map/state. Keeping them external means dynamic `import()`s of
316
+ // *.api.js / *.action.js bundles share the same ActionService instance
317
+ // that the server registered actions into at startup.
318
+ external: (id) => {
319
+ if (id.startsWith('node:') || NODE_BUILTINS.has(id)) return true
320
+ if (id === 'sharp' || id.startsWith('@img/sharp-')) return true
321
+ // Singleton packages — must not be bundled into entry chunks
322
+ if (id === '@ossy/platform' || id.startsWith('@ossy/platform/')) return true
323
+ if (id === '@ossy/event-store' || id.startsWith('@ossy/event-store/')) return true
324
+ if (id === '@ossy/observability' || id.startsWith('@ossy/observability/')) return true
325
+ return false
326
+ },
301
327
  plugins: [
302
328
  replace({
303
329
  preventAssignment: true,
@@ -11,9 +11,10 @@ export const INTEGRATION_FILE_PATTERN = /\.integration\.(mjs|cjs|js)$/
11
11
  export const STARTUP_FILE_PATTERN = /\.startup\.(mjs|cjs|js)$/
12
12
  export const EMAIL_FILE_PATTERN = /\.email\.(jsx?|tsx?)$/
13
13
  export const ACTION_FILE_PATTERN = /\.action\.(mjs|cjs|js)$/
14
+ export const E2E_FILE_PATTERN = /\.e2e\.(mjs|cjs|js)$/
14
15
 
15
16
  /**
16
- * @typedef {'page' | 'api' | 'task' | 'resource' | 'component' | 'aggregate' | 'integration' | 'startup' | 'email' | 'action'} EntryKind
17
+ * @typedef {'page' | 'api' | 'task' | 'resource' | 'component' | 'aggregate' | 'integration' | 'startup' | 'email' | 'action' | 'e2e'} EntryKind
17
18
  *
18
19
  * @typedef {object} PlatformEntry
19
20
  * @property {EntryKind} kind Which bucket this entry lives in.
@@ -32,6 +33,7 @@ export const ACTION_FILE_PATTERN = /\.action\.(mjs|cjs|js)$/
32
33
  * @property {PlatformEntry[]} startups Discovered `*.startup.{js,mjs,cjs}` entries.
33
34
  * @property {PlatformEntry[]} emails Discovered `*.email.{jsx,tsx,...}` entries.
34
35
  * @property {PlatformEntry[]} actions Discovered `*.action.{js,mjs,cjs}` entries.
36
+ * @property {PlatformEntry[]} e2es Discovered `*.e2e.{js,mjs,cjs}` entries (installed packages only).
35
37
  */
36
38
  export function discoverFilesByPattern (srcDir, filePattern) {
37
39
  const dir = path.resolve(srcDir)
@@ -60,6 +62,7 @@ function classifyFile (absPath) {
60
62
  if (STARTUP_FILE_PATTERN.test(base)) return 'startup'
61
63
  if (EMAIL_FILE_PATTERN.test(base)) return 'email'
62
64
  if (ACTION_FILE_PATTERN.test(base)) return 'action'
65
+ if (E2E_FILE_PATTERN.test(base)) return 'e2e'
63
66
  return null
64
67
  }
65
68
 
@@ -134,6 +137,7 @@ export function discoverInstalledPackageEntries (projectRoot) {
134
137
  ...discoverFilesByPattern(packageSrcDir, STARTUP_FILE_PATTERN),
135
138
  ...discoverFilesByPattern(packageSrcDir, EMAIL_FILE_PATTERN),
136
139
  ...discoverFilesByPattern(packageSrcDir, ACTION_FILE_PATTERN),
140
+ ...discoverFilesByPattern(packageSrcDir, E2E_FILE_PATTERN),
137
141
  ]
138
142
  for (const sourcePath of files) {
139
143
  const stat = fs.statSync(sourcePath)
@@ -177,12 +181,13 @@ export function discoverInstalledPackageEntries (projectRoot) {
177
181
  * @returns {Promise<PlatformFiles>}
178
182
  */
179
183
  export default async function getPlatformFiles (srcDir) {
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' }
184
+ const out = { pages: [], apis: [], tasks: [], resources: [], components: [], aggregates: [], integrations: [], startups: [], emails: [], actions: [], e2es: [] }
185
+ const bucketByKind = { page: 'pages', api: 'apis', task: 'tasks', resource: 'resources', component: 'components', aggregate: 'aggregates', integration: 'integrations', startup: 'startups', email: 'emails', action: 'actions', e2e: 'e2es' }
182
186
 
183
- // Aggregates are only discovered from installed packages — never from the
184
- // local `src/` — to avoid pulling in per-project duplicates of the same
185
- // aggregate class that is canonically owned by the feature package.
187
+ // Aggregates and e2e tests are only discovered from installed packages — never
188
+ // from the local `src/` — to avoid pulling in per-project duplicates of the
189
+ // same aggregate class or test suite that is canonically owned by the feature
190
+ // package.
186
191
  const localFiles = [
187
192
  ...discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN),
188
193
  ...discoverFilesByPattern(srcDir, API_FILE_PATTERN),
@@ -198,7 +203,7 @@ export default async function getPlatformFiles (srcDir) {
198
203
  const stat = fs.statSync(sourcePath)
199
204
  if (!stat.isFile() || stat.size === 0) continue
200
205
  const kind = classifyFile(sourcePath)
201
- if (!kind || kind === 'aggregate') continue
206
+ if (!kind || kind === 'aggregate' || kind === 'e2e') continue
202
207
  out[bucketByKind[kind]].push({ kind, sourcePath })
203
208
  }
204
209
 
@@ -64,6 +64,12 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
64
64
  * @property {string} entry URL the platform serves the action bundle from.
65
65
  * @property {string} access Access level: `'public'` | `'authenticated'` | `'workspace'`.
66
66
  *
67
+ * @typedef {object} E2eManifestEntry
68
+ * @property {string} id Unique test id (e.g. `'authentication/sign-in'`).
69
+ * @property {string} feature Grouping label (e.g. `'authentication'`).
70
+ * @property {string[]} requires Runtime dependencies needed (e.g. `['server', 'database']`).
71
+ * @property {string} entry URL the platform serves the e2e bundle from.
72
+ *
67
73
  * @typedef {object} Manifest
68
74
  * @property {ManifestEntry[]} entries Flat, ordered list of every routable thing.
69
75
  * @property {ComponentManifestEntry[]} components Discovered `*.component.*` entries, keyed by id.
@@ -73,6 +79,7 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
73
79
  * @property {StartupManifestEntry[]} startups Discovered `*.startup.js` entries.
74
80
  * @property {EmailManifestEntry[]} emails Discovered `*.email.{jsx,tsx}` entries.
75
81
  * @property {ActionManifestEntry[]} actions Discovered `*.action.js` entries.
82
+ * @property {E2eManifestEntry[]} e2es Discovered `*.e2e.js` entries from installed packages.
76
83
  * @property {object} config Inlined `src/config.js` default export.
77
84
  */
78
85
 
@@ -137,6 +144,9 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
137
144
  const seenStartupIds = new Set()
138
145
  const seenEmailIds = new Set()
139
146
  const seenActionIds = new Set()
147
+ /** @type {E2eManifestEntry[]} */
148
+ const e2es = []
149
+ const seenE2eIds = new Set()
140
150
 
141
151
  for (const fileName of Object.keys(bundle)) {
142
152
  const chunk = bundle[fileName]
@@ -286,6 +296,39 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
286
296
  continue
287
297
  }
288
298
 
299
+ // E2e tests contain Playwright test logic. The bundle exports `id`
300
+ // (a unique namespaced slug), `feature` (grouping label), `requires`
301
+ // (dependency list), and a default export (the async test function).
302
+ // They don't produce routable entries; they accumulate into a separate
303
+ // `e2es` array so test runners can discover and execute them.
304
+ if (entryInfo.kind === 'e2e') {
305
+ const e2eId = mod && mod.id
306
+ const feature = mod && mod.feature
307
+ const requires = mod && mod.requires
308
+ const testFn = mod && mod.default
309
+ if (typeof e2eId !== 'string' || e2eId.trim() === '') {
310
+ this.error(
311
+ `[@ossy/app][build] e2e entry ${entryInfo.sourcePath} must export a non-empty string "id"`,
312
+ )
313
+ }
314
+ if (typeof testFn !== 'function') {
315
+ this.error(
316
+ `[@ossy/app][build] e2e entry ${entryInfo.sourcePath} must default-export a function`,
317
+ )
318
+ }
319
+ if (seenE2eIds.has(e2eId)) {
320
+ this.error(`[@ossy/app][build] Duplicate e2e id "${e2eId}"`)
321
+ }
322
+ seenE2eIds.add(e2eId)
323
+ e2es.push({
324
+ id: e2eId,
325
+ feature: typeof feature === 'string' ? feature : '',
326
+ requires: Array.isArray(requires) ? requires : [],
327
+ entry: url,
328
+ })
329
+ continue
330
+ }
331
+
289
332
  // Resources are pure data — the default export *is* the template.
290
333
  // They don't use `metadata`, don't need a derived id, and don't
291
334
  // produce a routable manifest entry; they accumulate into a separate
@@ -347,7 +390,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
347
390
  }
348
391
 
349
392
  /** @type {Manifest} */
350
- const manifest = { entries, components, resourceTemplates, aggregates, integrations, startups, emails, actions, config: configValue || {} }
393
+ const manifest = { entries, components, resourceTemplates, aggregates, integrations, startups, emails, actions, e2es, config: configValue || {} }
351
394
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
352
395
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
353
396
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "1.36.0",
3
+ "version": "1.37.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.36.0",
41
+ "@ossy/design-system": "^1.37.0",
42
42
  "@ossy/pages": "^1.23.0",
43
- "@ossy/platform": "^1.35.0",
44
- "@ossy/router": "^1.36.0",
45
- "@ossy/router-react": "^1.36.0",
46
- "@ossy/sdk": "^1.36.0",
47
- "@ossy/sdk-react": "^1.36.0",
48
- "@ossy/themes": "^1.36.0",
43
+ "@ossy/platform": "^1.36.0",
44
+ "@ossy/router": "^1.37.0",
45
+ "@ossy/router-react": "^1.37.0",
46
+ "@ossy/sdk": "^1.37.0",
47
+ "@ossy/sdk-react": "^1.37.0",
48
+ "@ossy/themes": "^1.37.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": "24df2bde0d5d8794c5a82b9e802a2594ca73a5d9"
82
+ "gitHead": "6c7724dea1b5eda89e83319dce69703d622aadb1"
83
83
  }