@ossy/app 1.36.1 → 1.37.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.
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,
@@ -205,6 +206,18 @@ function generateActionStub ({ stubAbs, sourceAbs }) {
205
206
  ].join('\n')
206
207
  }
207
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
+
208
221
  function stubFor (kind, args) {
209
222
  if (kind === 'page') return generatePageStub(args)
210
223
  if (kind === 'api') return generateApiStub(args)
@@ -215,6 +228,7 @@ function stubFor (kind, args) {
215
228
  if (kind === 'startup') return generateStartupStub(args)
216
229
  if (kind === 'email') return generateEmailStub(args)
217
230
  if (kind === 'action') return generateActionStub(args)
231
+ if (kind === 'e2e') return generateE2eStub(args)
218
232
  return generateTaskStub(args)
219
233
  }
220
234
 
@@ -261,6 +275,7 @@ export async function build (cliArgs = []) {
261
275
  ...platformFiles.startups,
262
276
  ...platformFiles.emails,
263
277
  ...platformFiles.actions,
278
+ ...platformFiles.e2es,
264
279
  ]
265
280
 
266
281
  const entriesByStub = new Map()
@@ -295,7 +310,20 @@ export async function build (cliArgs = []) {
295
310
  // bundled task files at build time — bundling sharp would trigger its binary
296
311
  // loader during the build, which fails on linux-arm64 builders that don't
297
312
  // have the darwin binaries installed.
298
- 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
+ },
299
327
  plugins: [
300
328
  replace({
301
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.1",
3
+ "version": "1.37.1",
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.1",
41
+ "@ossy/design-system": "^1.37.1",
42
42
  "@ossy/pages": "^1.23.0",
43
- "@ossy/platform": "^1.35.1",
44
- "@ossy/router": "^1.36.1",
45
- "@ossy/router-react": "^1.36.1",
46
- "@ossy/sdk": "^1.36.1",
47
- "@ossy/sdk-react": "^1.36.1",
48
- "@ossy/themes": "^1.36.1",
43
+ "@ossy/platform": "^1.36.1",
44
+ "@ossy/router": "^1.37.1",
45
+ "@ossy/router-react": "^1.37.1",
46
+ "@ossy/sdk": "^1.37.1",
47
+ "@ossy/sdk-react": "^1.37.1",
48
+ "@ossy/themes": "^1.37.1",
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": "a144d7767264d96bc6ae095784d4f884f03a89a0"
82
+ "gitHead": "d5b71d16ec56c819401c7a74f95d12b2347ca3c4"
83
83
  }