diff --git a/.snyk b/.snyk index 1e83d06a5..c74c63314 100644 --- a/.snyk +++ b/.snyk @@ -1,5 +1,10 @@ version: v1.5.0 ignore: + 'SNYK-JS-BRACEEXPANSION-17706650': + - '* > serve-handler@6.1.7 > minimatch@3.1.5 > brace-expansion': + reason: 'ReDoS is fixed only in brace-expansion@5.0.7, whose CJS build uses a named export (exports.expand). Docusaurus'' serve-handler pins minimatch@3.1.5, which does `var expand = require(''brace-expansion'')` and calls it directly, so 5.0.7 throws "expand is not a function". minimatch 3.x cannot be upgraded (serve-handler needs its function-default API). Every other project in the repo is on the fixed brace-expansion@5.0.7 via overrides; this path is docs-only, build-time, over repository-controlled input, and not exploitable.' + expires: '2026-10-01T00:00:00.000Z' + created: '2026-07-01T00:00:00.000Z' 'SNYK-JS-SIRV-12558119': - '* > sirv@2.0.4': reason: 'Transitive dependency in Docusaurus; not exploitable in static site serving context (dev-only asset handler)' @@ -93,8 +98,8 @@ ignore: created: '2026-06-08T00:00:00.000Z' 'SNYK-JS-OPENTELEMETRYEXPORTERPROMETHEUS-16758050': - '* > @opentelemetry/exporter-prometheus@0.57.2': - reason: 'Requires upgrade of @opentelemetry/sdk-node to 0.217.0, which has type errors that break compilation. Created task to upgrade OTEL service to 2.x and resolve vulnerability that way.' - expires: '2026-07-28T00:00:00.000Z' + reason: 'Requires upgrade of @opentelemetry/sdk-node to 0.217.0, which has type errors that break compilation. The OTEL 2.x migration was attempted and reverted (it introduced unrelated churn and broke the coverage gate); staying on the 1.x line for now, so this ignore must persist until the migration is properly scoped into its own change.' + expires: '2026-10-01T00:00:00.000Z' created: '2026-06-01T10:00:00.000Z' 'SNYK-JS-POSTCSSSELECTORPARSER-16873882': - '* > postcss-selector-parser': @@ -165,11 +170,11 @@ ignore: created: '2026-06-18T00:00:00.000Z' 'SNYK-JS-OPENTELEMETRYCORE-17373280': - '* > @opentelemetry/core': - reason: 'The fix requires migrating the Azure Functions telemetry stack from OpenTelemetry 1.x to 2.8.0 or newer. The current Azure exporter and 0.57.x SDK packages require the 1.x API family; a forced major override is type-compatible at build time but leaves mixed SDK internals. Accepted temporarily while the existing OTEL 2.x migration is completed.' - expires: '2026-07-18T00:00:00.000Z' + reason: 'The fix requires migrating the Azure Functions telemetry stack from OpenTelemetry 1.x to 2.8.0 or newer. The current Azure exporter and 0.57.x SDK packages require the 1.x API family; a forced major override is type-compatible at build time but leaves mixed SDK internals. The OTEL 2.x migration was attempted and reverted, so this remains accepted until the migration lands as its own properly scoped change.' + expires: '2026-10-01T00:00:00.000Z' created: '2026-06-18T00:00:00.000Z' - 'SNYK-JS-BRACEEXPANSION-17706650': - - '* > brace-expansion@1.1.13': - reason: 'Transitive in Docusaurus serve-handler for local static docs serving. The fixed brace-expansion 5.x line requires minimatch 10, whose named-export API is incompatible with serve-handler 6.1.7.' - expires: '2026-09-30T00:00:00.000Z' - created: '2026-06-30T00:00:00.000Z' + 'SNYK-JS-OPENTELEMETRYPROPAGATORJAEGER-17901201': + - '* > @opentelemetry/propagator-jaeger': + reason: 'Reintroduced by staying on the OpenTelemetry 1.x SDK line: @opentelemetry/sdk-trace-node@1.30.1 pulls @opentelemetry/propagator-jaeger@1.30.1 transitively. Fixed only in 2.9.0, which requires the OTEL 2.x migration that was reverted here (it bundled unrelated dependency churn and broke the new-code coverage gate). The Jaeger propagator is not used by the Azure Functions telemetry stack, so this is not exploitable in current usage. Accepted until the 2.x migration lands as its own scoped change; tracked alongside SNYK-JS-OPENTELEMETRYCORE-17373280 and SNYK-JS-OPENTELEMETRYEXPORTERPROMETHEUS-16758050.' + expires: '2026-10-01T00:00:00.000Z' + created: '2026-07-14T00:00:00.000Z' diff --git a/apps/api/package.json b/apps/api/package.json index 9702bbf82..3751437d6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -47,6 +47,7 @@ "@opentelemetry/api": "1.9.0" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-rolldown": "workspace:*", "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", diff --git a/apps/api/src/archunit-tests/api-composition.test.ts b/apps/api/src/archunit-tests/api-composition.test.ts new file mode 100644 index 000000000..6f1ef5980 --- /dev/null +++ b/apps/api/src/archunit-tests/api-composition.test.ts @@ -0,0 +1,5 @@ +import { describeApiCompositionTests } from '@cellix/archunit-tests/api'; + +describeApiCompositionTests({ + apiIndexPath: 'src/index.ts', +}); diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index 1055de59f..6bdbc2e3e 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -14,6 +14,7 @@ "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", + "test:arch": "vitest run --config vitest.arch.config.ts", "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest", "storybook": "storybook dev -p 6008", @@ -38,6 +39,7 @@ "react-router-dom": "catalog:" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", "@cellix/local-dev": "workspace:*", diff --git a/apps/ui-community/src/archunit-tests/ui-app-composition.test.ts b/apps/ui-community/src/archunit-tests/ui-app-composition.test.ts new file mode 100644 index 000000000..6c4afc0eb --- /dev/null +++ b/apps/ui-community/src/archunit-tests/ui-app-composition.test.ts @@ -0,0 +1,6 @@ +import { describeUiAppCompositionTests } from '@cellix/archunit-tests/ui-app'; + +describeUiAppCompositionTests({ + appRoot: 'src', + requiredProviders: ['React.StrictMode', 'ThemeProvider', 'ConfigProvider', 'AntdApp', 'HelmetProvider', 'BrowserRouter', 'AuthProvider'], +}); diff --git a/apps/ui-community/vitest.arch.config.ts b/apps/ui-community/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/apps/ui-community/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index df6341849..25902f1fb 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -14,6 +14,7 @@ "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", + "test:arch": "vitest run --config vitest.arch.config.ts", "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, @@ -39,6 +40,7 @@ "react-router-dom": "catalog:" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", "@cellix/local-dev": "workspace:*", diff --git a/apps/ui-staff/src/archunit-tests/ui-app-composition.test.ts b/apps/ui-staff/src/archunit-tests/ui-app-composition.test.ts new file mode 100644 index 000000000..6c4afc0eb --- /dev/null +++ b/apps/ui-staff/src/archunit-tests/ui-app-composition.test.ts @@ -0,0 +1,6 @@ +import { describeUiAppCompositionTests } from '@cellix/archunit-tests/ui-app'; + +describeUiAppCompositionTests({ + appRoot: 'src', + requiredProviders: ['React.StrictMode', 'ThemeProvider', 'ConfigProvider', 'AntdApp', 'HelmetProvider', 'BrowserRouter', 'AuthProvider'], +}); diff --git a/apps/ui-staff/vitest.arch.config.ts b/apps/ui-staff/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/apps/ui-staff/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/packages/cellix/archunit-tests/manifest.md b/packages/cellix/archunit-tests/manifest.md new file mode 100644 index 000000000..54ceaa40b --- /dev/null +++ b/packages/cellix/archunit-tests/manifest.md @@ -0,0 +1,55 @@ +# @cellix/archunit-tests package manifest + +## Purpose + +Provide reusable architectural fitness checks that Cellix applications and libraries can register in their own Vitest suites. The package turns framework composition and organization conventions into executable, consumer-owned contracts. + +## Scope + +The package inspects TypeScript project source and registers Vitest architecture suites. It covers general framework composition, infrastructure initialization, package-layer conventions, and configurable source organization. + +## Public API shape + +Each focused export module exposes configuration types, asynchronous checkers that return all discovered violations, and `describe...Tests` helpers that register the checker with Vitest. + +## Core concepts + +- A checker reports violations without loading consumer runtime modules. +- A suite helper lets each consumer own and run its architecture contract. +- UI composition checks require configured providers to occur on the rendered component path to `App` and missing-root guards to terminate the invalid path. +- Configuration makes project paths and providers explicit, while Serenity bounded contexts are discovered from the consumer's standard context directory instead of embedding OCom-specific names in reusable rules. + +## Intended consumers + +- Cellix-based API and UI application entrypoints +- Cellix-based Serenity/Cucumber acceptance and end-to-end suites +- Cellix library packages that opt into the existing domain, persistence, GraphQL, application-service, frontend, or general conventions + +## Package boundaries + +- Public modules expose configuration types, violation-returning checkers, and Vitest suite registration helpers. +- Checks inspect consumer-owned source trees and do not import or execute consumer runtime code. +- Application-specific paths and bounded-context names are supplied by consumers. + +## Non-goals + +- Owning OCom-specific business rules or package names +- Generating application code +- Replacing behavioral, acceptance, or end-to-end tests +- Enforcing one fixed monorepo topology where a configurable project path is sufficient + +## Dependencies / relationships + +The package depends on Node filesystem APIs, Vitest for suite registration, and ArchUnit for established dependency checks. Consumer packages depend on a focused package subpath and provide only their own source paths and conventions. + +## Testing strategy + +Public contract tests exercise success and violation cases through module entrypoints. Real API, UI, acceptance, and E2E packages then register the reusable suites as downstream architecture tests. + +## Documentation obligations + +Every new public module must be represented in this manifest and the consumer README. Public functions require parameter, return, and usage documentation at their declaration. + +## Release-readiness standards + +The package build, full package test suite, and representative downstream consumer suites must pass. Additive exports must remain narrowly scoped; removals or behavior changes require downstream impact review and explicit migration guidance. diff --git a/packages/cellix/archunit-tests/package.json b/packages/cellix/archunit-tests/package.json index 96dcca505..ff57c093c 100644 --- a/packages/cellix/archunit-tests/package.json +++ b/packages/cellix/archunit-tests/package.json @@ -39,6 +39,18 @@ "./general": { "types": "./dist/src/general.d.ts", "default": "./dist/src/general.js" + }, + "./api": { + "types": "./dist/src/api.d.ts", + "default": "./dist/src/api.js" + }, + "./ui-app": { + "types": "./dist/src/ui-app.d.ts", + "default": "./dist/src/ui-app.js" + }, + "./serenity": { + "types": "./dist/src/serenity.d.ts", + "default": "./dist/src/serenity.js" } }, "scripts": { diff --git a/packages/cellix/archunit-tests/readme.md b/packages/cellix/archunit-tests/readme.md index 45619c042..db286f870 100644 --- a/packages/cellix/archunit-tests/readme.md +++ b/packages/cellix/archunit-tests/readme.md @@ -15,6 +15,49 @@ Current rules in this package include: - TypeScript compiler output conventions for `packages/cellix/*` - circular dependency checks across Cellix package source graphs - framework boundary checks, such as `ui-core` not depending on OCom UI code or app code +- API startup composition through infrastructure, context, application services, handlers, and startup, including service initialization during Azure `appStart` +- UI application bootstrap composition, including terminating missing-root validation, configured providers on the rendered path to `App`, and React Router +- Serenity/Cucumber managed-world initialization, concrete finalized server infrastructure, lifecycle cleanup, context loaders, and Screenplay delegation + +## Application Composition + +Register the API rule from the API package itself: + +```typescript +import { describeApiCompositionTests } from '@cellix/archunit-tests/api'; + +describeApiCompositionTests({ apiIndexPath: 'src/index.ts' }); +``` + +UI applications configure the providers their bootstrap is expected to compose around the rendered `App`. Providers may be nested through local wrapper components, but rendering one elsewhere does not satisfy the contract: + +```typescript +import { describeUiAppCompositionTests } from '@cellix/archunit-tests/ui-app'; + +describeUiAppCompositionTests({ + appRoot: 'src', + requiredProviders: ['ThemeProvider', 'BrowserRouter', 'AuthProvider'], +}); +``` + +## Serenity Suites + +Each acceptance or E2E package points the convention at its source root. Contexts are discovered automatically from the immediate directories under `src/contexts`. Infrastructure-backed suites can additionally require scenario cleanup and suite shutdown: + +```typescript +import { describeSerenitySuiteConventionTests } from '@cellix/archunit-tests/serenity'; + +describeSerenitySuiteConventionTests({ + suiteRoot: 'src', + requireManagedCleanup: true, +}); +``` + +The Serenity convention requires a managed world and lifecycle, a central context loader, context-local `*.steps.ts(x)` files, and steps that delegate through context-local Tasks, Interactions, or Questions. Suites that require managed cleanup must also export finalized `ApiInfrastructure` or `E2EInfrastructure` with at least one server; E2E infrastructure must include a UI portal. + +## Verifying the Architecture Tests + +The package includes intentionally incomplete portable projects under `src/fixtures/architecture-stubs/`. The package's normal architecture suite points each checker at its stub and asserts that the expected violations are reported. These test-of-the-tests run automatically through `test:arch`; no separate stub command or test area is required. ## What It Does Not Own diff --git a/packages/cellix/archunit-tests/src/api.ts b/packages/cellix/archunit-tests/src/api.ts new file mode 100644 index 000000000..169fa848b --- /dev/null +++ b/packages/cellix/archunit-tests/src/api.ts @@ -0,0 +1,3 @@ +export type { ApiCompositionConfig } from './checks/api-composition.js'; +export { checkApiComposition } from './checks/api-composition.js'; +export { describeApiCompositionTests } from './test-suites/api-composition.js'; diff --git a/packages/cellix/archunit-tests/src/application-composition.contract.test.ts b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts new file mode 100644 index 000000000..0fe6c9967 --- /dev/null +++ b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts @@ -0,0 +1,228 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { checkApiComposition } from './api.js'; +import { checkUiAppComposition } from './ui-app.js'; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true }))); +}); + +async function fixture(fileName: string, content: string): Promise { + const directory = await mkdtemp(path.join(tmpdir(), 'cellix-archunit-')); + temporaryDirectories.push(directory); + const filePath = path.join(directory, fileName); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, content); + return filePath; +} + +describe('application composition public contracts', () => { + it('accepts a complete Cellix API startup chain', async () => { + const apiIndexPath = await fixture( + 'index.ts', + `import { Cellix } from './cellix.ts'; +Cellix.initializeInfrastructureServices((registry) => registry.registerInfrastructureService(service)) + .setContext((registry) => { return { registry }; }) + .initializeApplicationServices((context) => build(context)) + .registerAzureFunctionHttpHandler('graphql', {}, handler) + .startUp();`, + ); + await writeFile( + path.join(path.dirname(apiIndexPath), 'cellix.ts'), + `export class Cellix { + private setupLifecycle() { + app.hook.appStart(async () => { + await this.startAllServicesWithTracing(); + this.contextInternal = this.contextCreatorInternal(this); + this.appServicesHostInternal = this.appServicesHostBuilder(this.contextInternal); + }); + } + private async startAllServicesWithTracing() { + await Promise.all(this.services.map((service) => service.startUp())); + } +}`, + ); + + expect(await checkApiComposition({ apiIndexPath })).toStrictEqual([]); + }); + + it('accepts aliased API composition imports and multiline callbacks', async () => { + const apiIndexPath = await fixture( + 'index.ts', + `import { Cellix as Application } from './cellix.ts'; +Application.initializeInfrastructureServices( + registry => registry.registerInfrastructureService(service), +) + .setContext(registry => ({ registry })) + .initializeApplicationServices(context => build(context)) + .registerAzureFunctionHttpHandler('graphql', {}, handler) + .startUp();`, + ); + await writeFile( + path.join(path.dirname(apiIndexPath), 'cellix.ts'), + `export class Cellix { + private setupLifecycle() { + app.hook.appStart(async () => { + await this.startAllServicesWithTracing(); + this.contextInternal = this.contextCreatorInternal(this); + }); + } +}`, + ); + + expect(await checkApiComposition({ apiIndexPath })).toStrictEqual([]); + }); + + it('reports omitted and out-of-order API startup stages', async () => { + const apiIndexPath = await fixture('index.ts', `import { Cellix } from './cellix.ts';\nCellix.startUp().setContext(() => ({}));`); + + const violations = await checkApiComposition({ apiIndexPath }); + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('initializeInfrastructureServices'), expect.stringContaining('initializeApplicationServices'), expect.stringContaining('registerAzureFunction')])); + }); + + it('reports a complete API startup chain composed in reverse order', async () => { + const apiIndexPath = await fixture( + 'index.ts', + `import { Cellix } from './cellix.ts'; +Cellix.startUp() + .registerAzureFunctionHttpHandler('graphql', {}, handler) + .initializeApplicationServices((context) => build(context)) + .setContext(() => ({})) + .initializeInfrastructureServices((registry) => registry.registerInfrastructureService(service));`, + ); + await writeFile( + path.join(path.dirname(apiIndexPath), 'cellix.ts'), + `export class Cellix { private setupLifecycle() { app.hook.appStart(async () => { await this.startAllServicesWithTracing(); this.contextInternal = this.contextCreatorInternal(this); }); } }`, + ); + + expect(await checkApiComposition({ apiIndexPath })).toEqual(expect.arrayContaining([expect.stringContaining('must be composed after')])); + }); + + it('reports a Cellix implementation that does not initialize infrastructure during app startup', async () => { + const apiIndexPath = await fixture( + 'index.ts', + `import { Cellix } from './cellix.ts'; +Cellix.initializeInfrastructureServices((registry) => registry.registerInfrastructureService(service)) + .setContext((registry) => { return { registry }; }) + .initializeApplicationServices((context) => build(context)) + .registerAzureFunctionHttpHandler('graphql', {}, handler) + .startUp();`, + ); + await writeFile(path.join(path.dirname(apiIndexPath), 'cellix.ts'), `export class Cellix { private setupLifecycle() { app.hook.appStart(async () => this.buildContext()); } }`); + + expect(await checkApiComposition({ apiIndexPath })).toEqual(expect.arrayContaining([expect.stringContaining('registered infrastructure services during appStart')])); + }); + + it('accepts a UI bootstrap with the configured providers and routed app', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App.tsx'; +const root = document.getElementById('root'); +if (!root) throw new Error('missing root'); +createRoot(root).render();`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot, requiredProviders: ['ThemeProvider', 'BrowserRouter'] })).toStrictEqual([]); + }); + + it('accepts aliased React Router components', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +if (mount === null) throw new Error('missing root'); +createRoot(mount).render();`, + ), + ); + await writeFile( + path.join(appRoot, 'App.tsx'), + `import { Routes as RouteSwitch, Route as RouteEntry } from 'react-router-dom'; +export default () => } />;`, + ); + + expect(await checkUiAppComposition({ appRoot })).toStrictEqual([]); + }); + + it('accepts an assertion helper that guards the mount point', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +invariant(mount, 'missing root'); +createRoot(mount).render();`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot })).toStrictEqual([]); + }); + + it('does not mistake an unrelated if and throw for mount-point validation', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +if (configuration.invalid) throw new Error('invalid configuration'); +createRoot(mount).render();`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot })).toContain(`[${path.join(appRoot, 'main.tsx')}] UI bootstrap must guard against a missing root element`); + }); + + it('does not mistake work performed when the mount point exists for a missing-root guard', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +if (mount) console.info('mount found'); +createRoot(mount).render();`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot })).toContain(`[${path.join(appRoot, 'main.tsx')}] UI bootstrap must guard against a missing root element`); + }); + + it('reports a required provider that does not wrap the rendered app', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +if (!mount) throw new Error('missing root'); +createRoot(mount).render(<>);`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot, requiredProviders: ['ThemeProvider'] })).toContain(`[${path.join(appRoot, 'main.tsx')}] UI bootstrap must compose ThemeProvider around App`); + }); + + it('reports a UI entrypoint that skips root validation, providers, or routing', async () => { + const appRoot = path.dirname(await fixture('src/main.tsx', `import { createRoot } from 'react-dom/client';\ncreateRoot(document.getElementById('root')).render();`)); + await writeFile(path.join(appRoot, 'App.tsx'), 'export default () =>
;'); + + const violations = await checkUiAppComposition({ appRoot, requiredProviders: ['ThemeProvider'] }); + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('guard'), expect.stringContaining('ThemeProvider'), expect.stringContaining('Routes')])); + }); +}); diff --git a/packages/cellix/archunit-tests/src/checks/api-composition.ts b/packages/cellix/archunit-tests/src/checks/api-composition.ts new file mode 100644 index 000000000..8c1c84356 --- /dev/null +++ b/packages/cellix/archunit-tests/src/checks/api-composition.ts @@ -0,0 +1,97 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { callbackContainsCall, callbackReturnsObject, callPositions, findImportedBinding, parseTypeScript } from './typescript-source.js'; + +export interface ApiCompositionConfig { + /** Path to the API composition root, normally `src/index.ts`. */ + apiIndexPath: string; +} + +const requiredStages = ['initializeInfrastructureServices', 'setContext', 'initializeApplicationServices', 'registerAzureFunction', 'startUp'] as const; + +/** + * Check that an API entry point composes every Cellix runtime layer in startup order. + * + * @param config - Location of the consumer's API composition entrypoint. + * @returns Every composition violation; an empty array means the entrypoint complies. + * @example + * ```typescript + * await checkApiComposition({ apiIndexPath: 'src/index.ts' }); + * ``` + */ +export async function checkApiComposition(config: ApiCompositionConfig): Promise { + if (!config.apiIndexPath) { + throw new Error('checkApiComposition requires apiIndexPath to be set'); + } + + let content: string; + try { + content = await readFile(config.apiIndexPath, 'utf8'); + } catch { + return [`[${config.apiIndexPath}] API composition root must exist`]; + } + + const violations: string[] = []; + const source = parseTypeScript(config.apiIndexPath, content); + const cellixBinding = findImportedBinding(source, 'Cellix'); + if (!cellixBinding) { + violations.push(`[${config.apiIndexPath}] API composition root must import Cellix`); + } + + const syntaxPositions = callPositions(source, requiredStages); + const positions = requiredStages.map((stage) => ({ stage, position: syntaxPositions.get(stage) ?? -1 })); + for (const { stage, position } of positions) { + if (position < 0) { + violations.push(`[${config.apiIndexPath}] API composition root must call ${stage}${stage === 'registerAzureFunction' ? '* for at least one handler' : ''}`); + } + } + + const presentPositions = positions.filter(({ position }) => position >= 0); + for (let index = 1; index < presentPositions.length; index += 1) { + const previous = presentPositions[index - 1]; + const current = presentPositions[index]; + if (previous && current && current.position < previous.position) { + violations.push(`[${config.apiIndexPath}] ${current.stage} must be composed after ${previous.stage}`); + } + } + + if (!callbackContainsCall(source, 'initializeInfrastructureServices', 'registerInfrastructureService')) { + violations.push(`[${config.apiIndexPath}] initializeInfrastructureServices must register at least one infrastructure service`); + } + if (!callbackReturnsObject(source, 'setContext')) { + violations.push(`[${config.apiIndexPath}] setContext must build and return the runtime context`); + } + + await checkCellixInfrastructureLifecycle(config.apiIndexPath, cellixBinding?.moduleName, violations); + + return violations; +} + +async function checkCellixInfrastructureLifecycle(apiIndexPath: string, importPath: string | undefined, violations: string[]): Promise { + if (!importPath) { + return; + } + if (!importPath.startsWith('.')) return; + + const cellixPath = path.resolve(path.dirname(apiIndexPath), importPath); + let cellix: string; + try { + cellix = await readFile(cellixPath, 'utf8'); + } catch { + violations.push(`[${cellixPath}] Cellix infrastructure lifecycle implementation must exist`); + return; + } + + const appStartPosition = cellix.indexOf('app.hook.appStart'); + const serviceStartupPosition = cellix.indexOf('startAllServicesWithTracing', appStartPosition); + const contextCreationPosition = cellix.indexOf('contextCreatorInternal', appStartPosition); + if (appStartPosition < 0 || serviceStartupPosition < 0) { + violations.push(`[${cellixPath}] Cellix must initialize registered infrastructure services during appStart`); + return; + } + if (contextCreationPosition < 0) { + violations.push(`[${cellixPath}] Cellix appStart must create the runtime context after infrastructure initialization`); + } else if (serviceStartupPosition > contextCreationPosition) { + violations.push(`[${cellixPath}] Cellix must initialize infrastructure services before creating the runtime context`); + } +} diff --git a/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts b/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts new file mode 100644 index 000000000..7edeedb95 --- /dev/null +++ b/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts @@ -0,0 +1,201 @@ +import { access, readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { containsCall, containsNewExpression, localImportName, parseTypeScript } from './typescript-source.js'; + +export interface SerenitySuiteConventionsConfig { + /** Source root containing the suite world, lifecycle hooks, and contexts. */ + suiteRoot: string; + /** Require per-scenario cleanup and suite-level infrastructure shutdown. */ + requireManagedCleanup?: boolean; +} + +async function exists(filePath: string): Promise { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +/** + * Check the managed-world and Screenplay organization contract of a Serenity suite. + * + * @param config - Suite source root and cleanup policy. Context directories are discovered automatically. + * @returns Every initialization or organization violation; an empty array means the suite complies. + * @example + * ```typescript + * await checkSerenitySuiteConventions({ suiteRoot: 'src' }); + * ``` + */ +export async function checkSerenitySuiteConventions(config: SerenitySuiteConventionsConfig): Promise { + if (!config.suiteRoot) { + throw new Error('checkSerenitySuiteConventions requires suiteRoot to be set'); + } + const violations: string[] = []; + const worldPath = path.join(config.suiteRoot, 'world.ts'); + const lifecyclePath = path.join(config.suiteRoot, 'cucumber-lifecycle-hooks.ts'); + const loaderPath = path.join(config.suiteRoot, 'step-definitions', 'index.ts'); + const contextsRoot = path.join(config.suiteRoot, 'contexts'); + + for (const requiredPath of [worldPath, lifecyclePath, loaderPath]) { + if (!(await exists(requiredPath))) { + violations.push(`[${requiredPath}] Required Serenity suite file must exist`); + } + } + + if (await exists(worldPath)) { + const world = await readFile(worldPath, 'utf8'); + const source = parseTypeScript(worldPath, world); + const registerWorld = localImportName(source, '@cellix/serenity-framework/cucumber', 'registerManagedSerenityWorld'); + const serenityCast = localImportName(source, '@cellix/serenity-framework/serenity', 'SerenityCast'); + if (!registerWorld || !containsCall(source, registerWorld)) { + violations.push(`[${worldPath}] Managed world must use registerManagedSerenityWorld from @cellix/serenity-framework/cucumber`); + } + if (!serenityCast || !containsNewExpression(source, serenityCast)) { + violations.push(`[${worldPath}] Managed world must use SerenityCast from @cellix/serenity-framework/serenity`); + } + for (const required of ['createCast:', 'useNotepad: true']) { + if (!world.includes(required)) { + violations.push(`[${worldPath}] Managed world must include ${required}`); + } + } + if (!/export\s+const\s+\w+World\s*=/.test(world) || !/export\s+type\s+\w+World\s*=\s*InstanceType/.test(world)) { + violations.push(`[${worldPath}] Managed world must export both its constructor and InstanceType`); + } + } + + if (await exists(lifecyclePath)) { + const lifecycle = await readFile(lifecyclePath, 'utf8'); + const source = parseTypeScript(lifecyclePath, lifecycle); + const registerHooks = localImportName(source, '@cellix/serenity-framework/cucumber', 'registerWorldLifecycleHooks'); + const getTimeout = localImportName(source, '@cellix/serenity-framework/settings', 'getTimeout'); + if (!registerHooks || !containsCall(source, registerHooks)) { + violations.push(`[${lifecyclePath}] Lifecycle must use registerWorldLifecycleHooks from @cellix/serenity-framework/cucumber`); + } + if (!getTimeout || !containsCall(source, getTimeout)) { + violations.push(`[${lifecyclePath}] Lifecycle must use shared timeout settings`); + } + if (!containsCall(source, 'init')) { + violations.push(`[${lifecyclePath}] Lifecycle hooks must include world.init()`); + } + if (config.requireManagedCleanup && !containsCall(source, 'cleanup')) { + violations.push(`[${lifecyclePath}] Managed infrastructure suites must clean up the world after each scenario`); + } + if (config.requireManagedCleanup && !containsCall(source, 'stopAll')) { + violations.push(`[${lifecyclePath}] Managed infrastructure suites must stop all infrastructure after the suite`); + } + } + if (config.requireManagedCleanup) { + await checkManagedInfrastructure(config.suiteRoot, violations); + } + + const loader = (await exists(loaderPath)) ? await readFile(loaderPath, 'utf8') : ''; + const contexts = await discoverContexts(contextsRoot); + if (contexts.length === 0) { + violations.push(`[${contextsRoot}] Serenity suite must define at least one context directory`); + } + const contextViolations = await Promise.all(contexts.map((context) => checkContext(config.suiteRoot, context, loaderPath, loader))); + violations.push(...contextViolations.flat()); + + return violations; +} + +async function checkManagedInfrastructure(suiteRoot: string, violations: string[]): Promise { + const infrastructurePath = path.join(suiteRoot, 'infrastructure.ts'); + if (!(await exists(infrastructurePath))) { + violations.push(`[${infrastructurePath}] Managed Serenity suites must define their infrastructure`); + return; + } + + const infrastructure = await readFile(infrastructurePath, 'utf8'); + const frameworkImport = infrastructure.match(/import\s+\{\s*(E2EInfrastructure|ApiInfrastructure)\s*\}\s+from\s+['"]@cellix\/serenity-framework\/infrastructure\/(e2e|api)['"]/); + const infrastructureKind = frameworkImport?.[1]; + if (!infrastructureKind) { + violations.push(`[${infrastructurePath}] Managed infrastructure must use a Cellix Serenity infrastructure builder`); + return; + } + if (!new RegExp(`export\\s+const\\s+infrastructure\\s*=\\s*${infrastructureKind}\\.create\\s*\\(`).test(infrastructure)) { + violations.push(`[${infrastructurePath}] Managed infrastructure must export a created ${infrastructureKind}`); + } + if (!/\.addServer\s*\(/.test(infrastructure)) { + violations.push(`[${infrastructurePath}] Managed infrastructure must register at least one server`); + } + if (infrastructureKind === 'E2EInfrastructure' && !/\.addUiPortal\s*\(/.test(infrastructure)) { + violations.push(`[${infrastructurePath}] E2E infrastructure must register at least one UI portal`); + } + if (!/\.finalize\s*\(\s*\)/.test(infrastructure)) { + violations.push(`[${infrastructurePath}] Managed infrastructure registration must be finalized`); + } +} + +async function discoverContexts(contextsRoot: string): Promise { + try { + const entries = await readdir(contextsRoot, { withFileTypes: true }); + return entries + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + } catch { + return []; + } +} + +async function checkContext(suiteRoot: string, context: string, loaderPath: string, loader: string): Promise { + const violations: string[] = []; + const contextRoot = path.join(suiteRoot, 'contexts', context); + const contextIndex = path.join(contextRoot, 'step-definitions', 'index.ts'); + if (!loader.includes(`../contexts/${context}/step-definitions/index.ts`)) { + violations.push(`[${loaderPath}] Central loader must import the ${context} context step-definition index`); + } + if (!(await exists(contextIndex))) { + violations.push(`[${contextIndex}] Context step-definition index must exist`); + return violations; + } + + const stepDirectory = path.dirname(contextIndex); + const stepFiles = (await readdir(stepDirectory)).filter((name) => /\.steps\.tsx?$/.test(name)); + if (stepFiles.length === 0) { + violations.push(`[${stepDirectory}] Context must contain at least one *.steps.ts or *.steps.tsx file`); + } + + const indexContent = await readFile(contextIndex, 'utf8'); + const stepViolations = await Promise.all( + stepFiles.sort().map(async (stepFile) => { + const discovered: string[] = []; + if (!indexContent.includes(`./${stepFile}`)) { + discovered.push(`[${contextIndex}] Context index must import ${stepFile}`); + } + const stepPath = path.join(stepDirectory, stepFile); + const stepContent = await readFile(stepPath, 'utf8'); + if (!/from\s+['"]@cucumber\/cucumber['"]/.test(stepContent)) { + discovered.push(`[${stepPath}] Step definitions must use Cucumber`); + } + const abstractionImports = [...stepContent.matchAll(/from\s+['"]\.\.\/(tasks|questions)\/([^'"]+)['"]/g)]; + if (abstractionImports.length === 0 || !/\.(attemptsTo|answer)\s*\(/.test(stepContent)) { + discovered.push(`[${stepPath}] Step definitions must delegate behavior to a task, interaction, or question`); + } + const abstractionViolations = await Promise.all( + abstractionImports.map(async (match) => { + const abstractionViolations: string[] = []; + const relativeFile = match[2]; + if (!relativeFile) return abstractionViolations; + const abstractionPath = path.join(contextRoot, match[1] as string, relativeFile); + if (!(await exists(abstractionPath))) { + abstractionViolations.push(`[${abstractionPath}] Imported Screenplay abstraction must exist`); + return abstractionViolations; + } + const abstraction = await readFile(abstractionPath, 'utf8'); + if (!/from\s+['"]@serenity-js\/core['"]/.test(abstraction) || !/\b(Task|Interaction|Question)\b/.test(abstraction)) { + abstractionViolations.push(`[${abstractionPath}] Context tasks and questions must implement a Serenity Screenplay abstraction`); + } + return abstractionViolations; + }), + ); + discovered.push(...abstractionViolations.flat()); + return discovered; + }), + ); + violations.push(...stepViolations.flat()); + return violations; +} diff --git a/packages/cellix/archunit-tests/src/checks/typescript-source.ts b/packages/cellix/archunit-tests/src/checks/typescript-source.ts new file mode 100644 index 000000000..8bceb264c --- /dev/null +++ b/packages/cellix/archunit-tests/src/checks/typescript-source.ts @@ -0,0 +1,225 @@ +import ts from 'typescript'; + +interface ImportedBinding { + imported: string; + local: string; +} + +export function parseTypeScript(filePath: string, content: string): ts.SourceFile { + return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, filePath.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS); +} + +function importedBindings(source: ts.SourceFile, moduleName: string): ImportedBinding[] { + const bindings: ImportedBinding[] = []; + for (const statement of source.statements) { + if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== moduleName) continue; + for (const element of statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) ? statement.importClause.namedBindings.elements : []) { + bindings.push({ imported: element.propertyName?.text ?? element.name.text, local: element.name.text }); + } + } + return bindings; +} + +export function localImportName(source: ts.SourceFile, moduleName: string, importedName: string): string | undefined { + return importedBindings(source, moduleName).find((binding) => binding.imported === importedName)?.local; +} + +export function findImportedBinding(source: ts.SourceFile, importedName: string): (ImportedBinding & { moduleName: string }) | undefined { + for (const statement of source.statements) { + if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue; + const binding = importedBindings(source, statement.moduleSpecifier.text).find((candidate) => candidate.imported === importedName); + if (binding) return { ...binding, moduleName: statement.moduleSpecifier.text }; + } + return undefined; +} + +export function containsNewExpression(source: ts.Node, name: string): boolean { + return someNode(source, (node) => ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name); +} + +export function containsCall(source: ts.Node, name: string): boolean { + return someNode(source, (node) => { + if (!ts.isCallExpression(node)) return false; + return expressionName(node.expression) === name; + }); +} + +export function callPositions(source: ts.SourceFile, names: readonly string[]): Map { + const positions = new Map(); + someNode(source, (node) => { + if (!ts.isCallExpression(node)) return false; + const name = expressionName(node.expression); + const matchedName = name && names.find((candidate) => name === candidate || (candidate === 'registerAzureFunction' && name.startsWith(candidate))); + if (matchedName && !positions.has(matchedName)) { + const positionNode = ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression; + positions.set(matchedName, positionNode.getStart(source)); + } + return false; + }); + return positions; +} + +export function callbackContainsCall(source: ts.SourceFile, callName: string, nestedCallName: string): boolean { + let matches = false; + someNode(source, (node) => { + if (!ts.isCallExpression(node) || expressionName(node.expression) !== callName) return false; + matches = node.arguments.some((argument) => (ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) && containsCall(argument.body, nestedCallName)); + return matches; + }); + return matches; +} + +export function callbackReturnsObject(source: ts.SourceFile, callName: string): boolean { + let matches = false; + someNode(source, (node) => { + if (!ts.isCallExpression(node) || expressionName(node.expression) !== callName) return false; + matches = node.arguments.some((argument) => { + if (!ts.isArrowFunction(argument) && !ts.isFunctionExpression(argument)) return false; + if (ts.isParenthesizedExpression(argument.body)) return ts.isObjectLiteralExpression(argument.body.expression); + if (ts.isObjectLiteralExpression(argument.body)) return true; + return ts.isBlock(argument.body) && argument.body.statements.some((statement) => ts.isReturnStatement(statement) && !!statement.expression && ts.isObjectLiteralExpression(statement.expression)); + }); + return matches; + }); + return matches; +} + +export function containsJsxTag(source: ts.SourceFile, tagName: string): boolean { + return someNode(source, (node) => { + if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) return node.tagName.getText(source) === tagName; + return false; + }); +} + +/** Check whether a JSX component is on a rendered path between the React root and App. */ +export function containsRenderedJsxWrapper(source: ts.SourceFile, wrapperName: string, childName = 'App'): boolean { + const root = '__render_root__'; + const edges = new Map>(); + const connect = (parent: string, child: string): void => { + const children = edges.get(parent) ?? new Set(); + children.add(child); + edges.set(parent, children); + }; + + someNode(source, (node) => { + const tag = jsxTagName(node, source); + if (!tag) return false; + + const parentTag = nearestJsxParentTag(node, source); + if (parentTag) { + connect(parentTag, tag); + } else { + const component = enclosingComponentName(node.parent); + if (component) connect(component, tag); + } + return false; + }); + + someNode(source, (node) => { + if (!ts.isCallExpression(node) || expressionName(node.expression) !== 'render') return false; + for (const argument of node.arguments) { + for (const tag of topLevelJsxTags(argument, source)) connect(root, tag); + } + return false; + }); + + return isReachable(edges, root, wrapperName) && isReachable(edges, wrapperName, childName); +} + +/** Check whether a resolved DOM mount point is protected by an explicit guard. */ +export function containsMountPointGuard(source: ts.SourceFile, elementId: string): boolean { + const mountNames = new Set(); + someNode(source, (node) => { + if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer) return false; + if (isElementLookup(node.initializer, elementId) || (ts.isCallExpression(node.initializer) && node.initializer.arguments.some((argument) => isElementLookup(argument, elementId)))) { + mountNames.add(node.name.text); + } + return false; + }); + + return someNode(source, (node) => { + if (ts.isIfStatement(node) && checksMissingValue(node.expression, mountNames)) { + return someNode(node.thenStatement, (child) => ts.isThrowStatement(child) || ts.isReturnStatement(child)); + } + if (!ts.isCallExpression(node) || !/^(assert|ensure|guard|invariant|require|validate)/i.test(expressionName(node.expression) ?? '')) return false; + return node.arguments.some((argument) => referencesAnyIdentifier(argument, mountNames) || isElementLookup(argument, elementId)); + }); +} + +function checksMissingValue(node: ts.Expression, names: ReadonlySet): boolean { + if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) { + return referencesAnyIdentifier(node.operand, names); + } + if (!ts.isBinaryExpression(node)) return false; + if (![ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(node.operatorToken.kind)) return false; + return (referencesAnyIdentifier(node.left, names) && isNullish(node.right)) || (isNullish(node.left) && referencesAnyIdentifier(node.right, names)); +} + +function isNullish(node: ts.Expression): boolean { + return node.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(node) && node.text === 'undefined'); +} + +function jsxTagName(node: ts.Node, source: ts.SourceFile): string | undefined { + if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) return node.tagName.getText(source); + return undefined; +} + +function nearestJsxParentTag(node: ts.Node | undefined, source: ts.SourceFile): string | undefined { + for (let current = node?.parent; current; current = current.parent) { + if (ts.isJsxElement(current) && current.openingElement !== node) return current.openingElement.tagName.getText(source); + if (ts.isFunctionLike(current)) return undefined; + } + return undefined; +} + +function enclosingComponentName(node: ts.Node | undefined): string | undefined { + for (let current = node; current; current = current.parent) { + if (ts.isFunctionDeclaration(current) && current.name) return current.name.text; + if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) && ts.isVariableDeclaration(current.parent) && ts.isIdentifier(current.parent.name)) return current.parent.name.text; + } + return undefined; +} + +function topLevelJsxTags(node: ts.Node, source: ts.SourceFile): string[] { + const tags: string[] = []; + const visit = (current: ts.Node): void => { + const tag = jsxTagName(current, source); + if (tag) { + tags.push(tag); + return; + } + current.forEachChild(visit); + }; + visit(node); + return tags; +} + +function isReachable(edges: ReadonlyMap>, from: string, to: string, seen = new Set()): boolean { + if (from === to) return true; + if (seen.has(from)) return false; + seen.add(from); + return [...(edges.get(from) ?? [])].some((child) => isReachable(edges, child, to, seen)); +} + +function isElementLookup(node: ts.Node, elementId: string): boolean { + return ts.isCallExpression(node) && expressionName(node.expression) === 'getElementById' && node.arguments.some((argument) => ts.isStringLiteral(argument) && argument.text === elementId); +} + +function referencesAnyIdentifier(node: ts.Node, names: ReadonlySet): boolean { + return someNode(node, (child) => ts.isIdentifier(child) && names.has(child.text)); +} + +function expressionName(expression: ts.LeftHandSideExpression): string | undefined { + if (ts.isIdentifier(expression)) return expression.text; + if (ts.isPropertyAccessExpression(expression)) return expression.name.text; + return undefined; +} + +function someNode(node: ts.Node, predicate: (node: ts.Node) => boolean): boolean { + if (predicate(node)) return true; + let found = false; + node.forEachChild((child) => { + if (!found && someNode(child, predicate)) found = true; + }); + return found; +} diff --git a/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts new file mode 100644 index 000000000..36a4c8296 --- /dev/null +++ b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts @@ -0,0 +1,76 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { containsCall, containsJsxTag, containsMountPointGuard, containsRenderedJsxWrapper, localImportName, parseTypeScript } from './typescript-source.js'; + +export interface UiAppCompositionConfig { + /** Directory containing `main.tsx` and `App.tsx`. */ + appRoot: string; + /** Provider component names that must wrap the application bootstrap. */ + requiredProviders?: string[]; +} + +async function readSource(filePath: string, violations: string[]): Promise { + try { + return await readFile(filePath, 'utf8'); + } catch { + violations.push(`[${filePath}] Required UI composition file must exist`); + return undefined; + } +} + +/** + * Check that a Cellix browser app validates its mount point and composes its configured providers and routes. + * + * @param config - Source root and provider names required by the consumer application. + * @returns Every bootstrap violation; an empty array means the UI composition complies. + * @example + * ```typescript + * await checkUiAppComposition({ appRoot: 'src', requiredProviders: ['BrowserRouter'] }); + * ``` + */ +export async function checkUiAppComposition(config: UiAppCompositionConfig): Promise { + if (!config.appRoot) { + throw new Error('checkUiAppComposition requires appRoot to be set'); + } + + const violations: string[] = []; + const mainPath = path.join(config.appRoot, 'main.tsx'); + const appPath = path.join(config.appRoot, 'App.tsx'); + const main = await readSource(mainPath, violations); + const app = await readSource(appPath, violations); + + if (main) { + const source = parseTypeScript(mainPath, main); + if (!containsCall(source, 'createRoot')) { + violations.push(`[${mainPath}] UI bootstrap must create a React root`); + } + if (!/getElementById\s*\(\s*['"]root['"]\s*\)/.test(main)) { + violations.push(`[${mainPath}] UI bootstrap must resolve the #root mount element`); + } + if (!containsMountPointGuard(source, 'root')) { + violations.push(`[${mainPath}] UI bootstrap must guard against a missing root element`); + } + if (!containsJsxTag(source, 'React.StrictMode')) { + violations.push(`[${mainPath}] UI bootstrap must enable React.StrictMode`); + } + if (!containsJsxTag(source, 'App')) { + violations.push(`[${mainPath}] UI bootstrap must render App`); + } + for (const provider of config.requiredProviders ?? []) { + if (!containsRenderedJsxWrapper(source, provider)) { + violations.push(`[${mainPath}] UI bootstrap must compose ${provider} around App`); + } + } + } + + if (app) { + const source = parseTypeScript(appPath, app); + const routes = localImportName(source, 'react-router-dom', 'Routes'); + const route = localImportName(source, 'react-router-dom', 'Route'); + if (!routes || !route || !containsJsxTag(source, routes) || !containsJsxTag(source, route)) { + violations.push(`[${appPath}] App must compose routes with React Router Routes and Route`); + } + } + + return violations; +} diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/api/src/cellix.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/api/src/cellix.ts new file mode 100644 index 000000000..85cd3566a --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/api/src/cellix.ts @@ -0,0 +1,6 @@ +// biome-ignore lint/complexity/noStaticOnlyClass: intentionally incomplete architecture stub +export class Cellix { + static startUp(): void { + // Intentionally omits infrastructure initialization. + } +} diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/api/src/index.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/api/src/index.ts new file mode 100644 index 000000000..c7444fdf0 --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/api/src/index.ts @@ -0,0 +1,3 @@ +import { Cellix } from './cellix.ts'; + +Cellix.startUp(); diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/community.steps.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/community.steps.ts new file mode 100644 index 000000000..ff6bfde60 --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/community.steps.ts @@ -0,0 +1,3 @@ +import { When } from '@cucumber/cucumber'; + +When('the stub performs behavior directly', async () => fetch('/api')); diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/index.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/index.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/cucumber-lifecycle-hooks.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/cucumber-lifecycle-hooks.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/cucumber-lifecycle-hooks.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/infrastructure.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/infrastructure.ts new file mode 100644 index 000000000..de7908ea8 --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/infrastructure.ts @@ -0,0 +1,3 @@ +import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; + +export const infrastructure = E2EInfrastructure.create().finalize(); diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/step-definitions/index.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/step-definitions/index.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/step-definitions/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/world.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/world.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/world.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/App.tsx b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/App.tsx new file mode 100644 index 000000000..9d6e50f4b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/App.tsx @@ -0,0 +1,3 @@ +export default function App() { + return null; +} diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/main.tsx b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/main.tsx new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/main.tsx @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/index.ts b/packages/cellix/archunit-tests/src/index.ts index 1385a951c..4e6b32aa5 100644 --- a/packages/cellix/archunit-tests/src/index.ts +++ b/packages/cellix/archunit-tests/src/index.ts @@ -1,3 +1,4 @@ +export * from './api.js'; export * from './application-services.js'; export * from './data-sources-mongoose-models.js'; export * from './domain.js'; @@ -5,3 +6,5 @@ export * from './frontend.js'; export * from './general.js'; export * from './graphql.js'; export * from './persistence.js'; +export * from './serenity.js'; +export * from './ui-app.js'; diff --git a/packages/cellix/archunit-tests/src/serenity.contract.test.ts b/packages/cellix/archunit-tests/src/serenity.contract.test.ts new file mode 100644 index 000000000..1c52f1177 --- /dev/null +++ b/packages/cellix/archunit-tests/src/serenity.contract.test.ts @@ -0,0 +1,110 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { checkSerenitySuiteConventions } from './serenity.js'; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true }))); +}); + +async function write(root: string, relativePath: string, content: string): Promise { + const filePath = path.join(root, relativePath); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, content); +} + +async function suiteFixture(): Promise { + const suiteRoot = await mkdtemp(path.join(tmpdir(), 'cellix-serenity-')); + temporaryDirectories.push(suiteRoot); + await write( + suiteRoot, + 'world.ts', + `import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber';\nimport { SerenityCast } from '@cellix/serenity-framework/serenity';\nimport { infrastructure } from './infrastructure.ts';\nexport const TestWorld = registerManagedSerenityWorld({ infrastructure, createCast: () => new SerenityCast({ useNotepad: true }) });\nexport type TestWorld = InstanceType;`, + ); + await write( + suiteRoot, + 'cucumber-lifecycle-hooks.ts', + `import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber';\nimport { getTimeout } from '@cellix/serenity-framework/settings';\nregisterWorldLifecycleHooks({ scenarioTimeout: getTimeout('scenario'), before: async (world) => world.init(), after: async (world) => world.cleanup(), afterAll: () => infrastructure.stopAll() });`, + ); + await write(suiteRoot, 'step-definitions/index.ts', `import '../contexts/community/step-definitions/index.ts';`); + await write(suiteRoot, 'contexts/community/step-definitions/index.ts', `import './create-community.steps.ts';`); + await write( + suiteRoot, + 'contexts/community/step-definitions/create-community.steps.ts', + `import { Given, When, Then } from '@cucumber/cucumber';\nimport { CreateCommunity } from '../tasks/create-community.ts';\nimport { CommunityName } from '../questions/community-name.ts';\nWhen('creates', async () => actor.attemptsTo(CreateCommunity()));\nThen('created', async () => actor.answer(CommunityName()));`, + ); + await write(suiteRoot, 'contexts/community/tasks/create-community.ts', `import { Interaction } from '@serenity-js/core';\nexport const CreateCommunity = () => Interaction.where('creates', async () => undefined);`); + await write(suiteRoot, 'contexts/community/questions/community-name.ts', `import { Question } from '@serenity-js/core';\nexport const CommunityName = () => Question.about('name', () => 'Cellix');`); + await write( + suiteRoot, + 'infrastructure.ts', + `import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; +export const infrastructure = E2EInfrastructure.create() + .addServer('api', apiServer) + .addUiPortal('community', communityPortal, { dependsOn: ['api'] }) + .finalize();`, + ); + return suiteRoot; +} + +describe('Serenity convention public contract', () => { + it('accepts managed lifecycle and context-owned Screenplay abstractions', async () => { + const suiteRoot = await suiteFixture(); + expect(await checkSerenitySuiteConventions({ suiteRoot, requireManagedCleanup: true })).toStrictEqual([]); + }); + + it('accepts aliased framework imports and formatted lifecycle callbacks', async () => { + const suiteRoot = await suiteFixture(); + await write( + suiteRoot, + 'world.ts', + `import { registerManagedSerenityWorld as registerWorld } from '@cellix/serenity-framework/cucumber'; +import { SerenityCast as TestCast } from '@cellix/serenity-framework/serenity'; +export const TestWorld = registerWorld({ createCast: () => new TestCast({ useNotepad: true }) }); +export type TestWorld = InstanceType;`, + ); + await write( + suiteRoot, + 'cucumber-lifecycle-hooks.ts', + `import { registerWorldLifecycleHooks as registerHooks } from '@cellix/serenity-framework/cucumber'; +import { getTimeout as timeoutFor } from '@cellix/serenity-framework/settings'; +registerHooks({ + scenarioTimeout: timeoutFor('scenario'), + before: async world => world.init(), + after: async world => world.cleanup(), + afterAll: async () => { + await infrastructure.stopAll(); + }, +});`, + ); + + expect(await checkSerenitySuiteConventions({ suiteRoot, requireManagedCleanup: true })).toStrictEqual([]); + }); + + it('reports missing context indexes and steps that contain behavior directly', async () => { + const suiteRoot = await suiteFixture(); + await write(suiteRoot, 'step-definitions/index.ts', ''); + await write(suiteRoot, 'contexts/community/step-definitions/create-community.steps.ts', `import { When } from '@cucumber/cucumber';\nWhen('creates', async () => fetch('/graphql'));`); + + const violations = await checkSerenitySuiteConventions({ suiteRoot }); + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('Central loader'), expect.stringContaining('delegate behavior')])); + }); + + it('reports a suite with no discoverable contexts', async () => { + const suiteRoot = await suiteFixture(); + await rm(path.join(suiteRoot, 'contexts'), { recursive: true }); + + expect(await checkSerenitySuiteConventions({ suiteRoot })).toContain(`[${path.join(suiteRoot, 'contexts')}] Serenity suite must define at least one context directory`); + }); + + it('reports managed infrastructure without concrete server and UI portal registration', async () => { + const suiteRoot = await suiteFixture(); + await write(suiteRoot, 'infrastructure.ts', `import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e';\nexport const infrastructure = E2EInfrastructure.create().finalize();`); + + const violations = await checkSerenitySuiteConventions({ suiteRoot, requireManagedCleanup: true }); + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('at least one server'), expect.stringContaining('at least one UI portal')])); + }); +}); diff --git a/packages/cellix/archunit-tests/src/serenity.ts b/packages/cellix/archunit-tests/src/serenity.ts new file mode 100644 index 000000000..bbd779e01 --- /dev/null +++ b/packages/cellix/archunit-tests/src/serenity.ts @@ -0,0 +1,3 @@ +export type { SerenitySuiteConventionsConfig } from './checks/serenity-suite-conventions.js'; +export { checkSerenitySuiteConventions } from './checks/serenity-suite-conventions.js'; +export { describeSerenitySuiteConventionTests } from './test-suites/serenity-suite-conventions.js'; diff --git a/packages/cellix/archunit-tests/src/stub-rejection.contract.test.ts b/packages/cellix/archunit-tests/src/stub-rejection.contract.test.ts new file mode 100644 index 000000000..0490e6ac3 --- /dev/null +++ b/packages/cellix/archunit-tests/src/stub-rejection.contract.test.ts @@ -0,0 +1,34 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { checkApiComposition } from './api.js'; +import { checkSerenitySuiteConventions } from './serenity.js'; +import { checkUiAppComposition } from './ui-app.js'; + +const fixturesRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'architecture-stubs'); + +describe('architecture test stub rejection', () => { + it('rejects the incomplete API stub', async () => { + const violations = await checkApiComposition({ apiIndexPath: path.join(fixturesRoot, 'api', 'src', 'index.ts') }); + + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('initializeInfrastructureServices'), expect.stringContaining('initializeApplicationServices'), expect.stringContaining('registerAzureFunction')])); + }); + + it('rejects the incomplete UI stub', async () => { + const violations = await checkUiAppComposition({ + appRoot: path.join(fixturesRoot, 'ui', 'src'), + requiredProviders: ['ThemeProvider', 'BrowserRouter', 'AuthProvider'], + }); + + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('React root'), expect.stringContaining('ThemeProvider'), expect.stringContaining('Routes')])); + }); + + it('rejects the incomplete Serenity stub', async () => { + const violations = await checkSerenitySuiteConventions({ + suiteRoot: path.join(fixturesRoot, 'serenity', 'src'), + requireManagedCleanup: true, + }); + + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('Managed world'), expect.stringContaining('at least one server'), expect.stringContaining('delegate behavior')])); + }); +}); diff --git a/packages/cellix/archunit-tests/src/test-suites/api-composition.ts b/packages/cellix/archunit-tests/src/test-suites/api-composition.ts new file mode 100644 index 000000000..2c4d11016 --- /dev/null +++ b/packages/cellix/archunit-tests/src/test-suites/api-composition.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { type ApiCompositionConfig, checkApiComposition } from '../checks/api-composition.js'; + +/** + * Register reusable fitness tests for a Cellix API composition root. + * @param config - Location of the consumer's API composition entrypoint. + * @returns Nothing; tests are registered with the active Vitest suite. + * @example `describeApiCompositionTests({ apiIndexPath: 'src/index.ts' });` + */ +export function describeApiCompositionTests(config: ApiCompositionConfig): void { + describe('Cellix API composition', () => { + it('initializes infrastructure, context, application services, handlers, and startup in order', async () => { + expect(await checkApiComposition(config)).toStrictEqual([]); + }); + }); +} diff --git a/packages/cellix/archunit-tests/src/test-suites/serenity-suite-conventions.ts b/packages/cellix/archunit-tests/src/test-suites/serenity-suite-conventions.ts new file mode 100644 index 000000000..fd9c1033c --- /dev/null +++ b/packages/cellix/archunit-tests/src/test-suites/serenity-suite-conventions.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { checkSerenitySuiteConventions, type SerenitySuiteConventionsConfig } from '../checks/serenity-suite-conventions.js'; + +/** + * Register reusable fitness tests for a Cellix Serenity/Cucumber suite. + * @param config - Suite source root and cleanup policy. Context directories are discovered automatically. + * @returns Nothing; tests are registered with the active Vitest suite. + * @example `describeSerenitySuiteConventionTests({ suiteRoot: 'src', requireManagedCleanup: true });` + */ +export function describeSerenitySuiteConventionTests(config: SerenitySuiteConventionsConfig): void { + describe('Cellix Serenity suite conventions', () => { + it('uses a managed world and lifecycle', async () => { + const violations = await checkSerenitySuiteConventions(config); + expect(violations.filter((violation) => violation.includes('world.ts') || violation.includes('lifecycle'))).toStrictEqual([]); + }); + + it('loads context indexes and delegates steps to Screenplay abstractions', async () => { + const violations = await checkSerenitySuiteConventions(config); + expect(violations.filter((violation) => !violation.includes('world.ts') && !violation.includes('lifecycle'))).toStrictEqual([]); + }); + }); +} diff --git a/packages/cellix/archunit-tests/src/test-suites/ui-app-composition.ts b/packages/cellix/archunit-tests/src/test-suites/ui-app-composition.ts new file mode 100644 index 000000000..ab689c9bc --- /dev/null +++ b/packages/cellix/archunit-tests/src/test-suites/ui-app-composition.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { checkUiAppComposition, type UiAppCompositionConfig } from '../checks/ui-app-composition.js'; + +/** + * Register reusable fitness tests for a Cellix browser application entrypoint. + * @param config - Source root and providers required by the consumer application. + * @returns Nothing; tests are registered with the active Vitest suite. + * @example `describeUiAppCompositionTests({ appRoot: 'src', requiredProviders: ['BrowserRouter'] });` + */ +export function describeUiAppCompositionTests(config: UiAppCompositionConfig): void { + describe('Cellix UI application composition', () => { + it('validates the mount point and composes providers, the app, and routes', async () => { + expect(await checkUiAppComposition(config)).toStrictEqual([]); + }); + }); +} diff --git a/packages/cellix/archunit-tests/src/ui-app.ts b/packages/cellix/archunit-tests/src/ui-app.ts new file mode 100644 index 000000000..6f8ee3f2d --- /dev/null +++ b/packages/cellix/archunit-tests/src/ui-app.ts @@ -0,0 +1,3 @@ +export type { UiAppCompositionConfig } from './checks/ui-app-composition.js'; +export { checkUiAppComposition } from './checks/ui-app-composition.js'; +export { describeUiAppCompositionTests } from './test-suites/ui-app-composition.js'; diff --git a/packages/cellix/archunit-tests/tsconfig.json b/packages/cellix/archunit-tests/tsconfig.json index 8dbf44f5b..7a0e79572 100644 --- a/packages/cellix/archunit-tests/tsconfig.json +++ b/packages/cellix/archunit-tests/tsconfig.json @@ -5,5 +5,5 @@ "rootDir": "." }, "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] + "exclude": ["node_modules", "dist", "**/*.test.ts", "src/fixtures/architecture-stubs/**"] } diff --git a/packages/cellix/serenity-framework/src/settings/timeout-settings.ts b/packages/cellix/serenity-framework/src/settings/timeout-settings.ts index 17abeeaac..1e20587b6 100644 --- a/packages/cellix/serenity-framework/src/settings/timeout-settings.ts +++ b/packages/cellix/serenity-framework/src/settings/timeout-settings.ts @@ -5,6 +5,15 @@ export const defaultVerificationTimeouts = { /** Default Cucumber scenario timeout. */ scenario: 120_000, + /** + * Cold-boot timeout for the first `Before` hook, which starts every server + * (Mongo, Azurite, API, UI portals, mock auth) and launches the browser. + * Larger than {@link serverStartup} because a cold run also pays for func + * TypeScript compilation and Vite dependency pre-bundling; only the first + * scenario incurs it (later `Before`s reuse the running infrastructure). + */ + boot: 300_000, + /** Server startup timeout. */ serverStartup: 120_000, diff --git a/packages/ocom-verification/acceptance-api/package.json b/packages/ocom-verification/acceptance-api/package.json index 06e434cb3..04216dafe 100644 --- a/packages/ocom-verification/acceptance-api/package.json +++ b/packages/ocom-verification/acceptance-api/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "scripts": { + "test:arch": "vitest run --config vitest.arch.config.ts", "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js --format json:./reports/cucumber-report-api.json", "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 --allowExternal -- cucumber-js --format json:./reports/cucumber-report-api.json", "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 -- cucumber-js --format json:./reports/cucumber-report-api.json", @@ -25,7 +26,9 @@ "std-env": "^4.0.0" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-typescript": "workspace:*", + "@cellix/config-vitest": "workspace:*", "@cellix/service-blob-storage": "workspace:*", "@ocom/application-services": "workspace:*", "@ocom/context-spec": "workspace:*", @@ -43,6 +46,7 @@ "c8": "^11.0.0", "rimraf": "^6.0.1", "tsx": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/ocom-verification/acceptance-api/src/archunit-tests/serenity-conventions.test.ts b/packages/ocom-verification/acceptance-api/src/archunit-tests/serenity-conventions.test.ts new file mode 100644 index 000000000..0459db109 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/archunit-tests/serenity-conventions.test.ts @@ -0,0 +1,6 @@ +import { describeSerenitySuiteConventionTests } from '@cellix/archunit-tests/serenity'; + +describeSerenitySuiteConventionTests({ + suiteRoot: 'src', + requireManagedCleanup: true, +}); diff --git a/packages/ocom-verification/acceptance-api/src/contexts/staff/notes/staff-notes.ts b/packages/ocom-verification/acceptance-api/src/contexts/staff/notes/staff-notes.ts new file mode 100644 index 000000000..4abd0118f --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/staff/notes/staff-notes.ts @@ -0,0 +1,3 @@ +export interface StaffApiNotes { + targetRoute: string; +} diff --git a/packages/ocom-verification/acceptance-api/src/contexts/staff/questions/staff-target-route.ts b/packages/ocom-verification/acceptance-api/src/contexts/staff/questions/staff-target-route.ts new file mode 100644 index 000000000..119a2807c --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/staff/questions/staff-target-route.ts @@ -0,0 +1,4 @@ +import { notes, Question } from '@serenity-js/core'; +import type { StaffApiNotes } from '../notes/staff-notes.ts'; + +export const StaffTargetRoute = () => Question.about('the target staff route', (actor) => actor.answer(notes().get('targetRoute'))); diff --git a/packages/ocom-verification/acceptance-api/src/contexts/staff/step-definitions/staff-landing.steps.ts b/packages/ocom-verification/acceptance-api/src/contexts/staff/step-definitions/staff-landing.steps.ts index 475769e42..303a82b36 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/staff/step-definitions/staff-landing.steps.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/staff/step-definitions/staff-landing.steps.ts @@ -1,13 +1,12 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { actors } from '@ocom-verification/verification-shared/test-data'; import { actorCalled, notes } from '@serenity-js/core'; +import type { StaffApiNotes } from '../notes/staff-notes.ts'; +import { StaffTargetRoute } from '../questions/staff-target-route.ts'; +import { OpenStaffLanding } from '../tasks/open-staff-landing.ts'; type StaffBusinessRole = 'finance' | 'tech admin' | 'service line owner' | 'case manager'; -interface StaffApiNotes { - targetRoute: string; -} - const defaultRouteByRole: Record = { finance: '/staff/finance', 'tech admin': '/staff/tech', @@ -51,19 +50,19 @@ Given('{word} is an authenticated {string} staff user', async (actorName: string When('{word} enters the staff operations workspace', async (actorName: string) => { lastActorName = actorName; const actor = actorCalled(actorName); - await actor.attemptsTo(notes().set('targetRoute', defaultRouteByRole[roleForActor(actorName)])); + await actor.attemptsTo(OpenStaffLanding(defaultRouteByRole[roleForActor(actorName)])); }); When('{word} attempts to work in the finance workspace', async (actorName: string) => { lastActorName = actorName; const actor = actorCalled(actorName); - await actor.attemptsTo(notes().set('targetRoute', resolveFinanceWorkspaceRoute(roleForActor(actorName)))); + await actor.attemptsTo(OpenStaffLanding(resolveFinanceWorkspaceRoute(roleForActor(actorName)))); }); Then('{word} should be directed to {string}', async (actorName: string, expectedRoute: string) => { const resolvedName = /^(she|he|they)$/i.test(actorName) ? lastActorName : actorName; const actor = actorCalled(resolvedName); - const targetRoute = await actor.answer(notes().get('targetRoute')); + const targetRoute = await actor.answer(StaffTargetRoute()); if (targetRoute !== expectedRoute) { throw new Error(`Expected route to be "${expectedRoute}", but got "${targetRoute}"`); diff --git a/packages/ocom-verification/acceptance-api/src/contexts/staff/tasks/open-staff-landing.ts b/packages/ocom-verification/acceptance-api/src/contexts/staff/tasks/open-staff-landing.ts new file mode 100644 index 000000000..6a482a8f6 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/staff/tasks/open-staff-landing.ts @@ -0,0 +1,7 @@ +import { type Actor, Interaction, notes } from '@serenity-js/core'; +import type { StaffApiNotes } from '../notes/staff-notes.ts'; + +export const OpenStaffLanding = (targetRoute: string) => + Interaction.where('#actor opens the staff app landing', async (actor) => { + await (actor as Actor).attemptsTo(notes().set('targetRoute', targetRoute)); + }); diff --git a/packages/ocom-verification/acceptance-api/vitest.arch.config.ts b/packages/ocom-verification/acceptance-api/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/packages/ocom-verification/acceptance-api/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/packages/ocom-verification/acceptance-ui/package.json b/packages/ocom-verification/acceptance-ui/package.json index 9cb328852..0defea12f 100644 --- a/packages/ocom-verification/acceptance-ui/package.json +++ b/packages/ocom-verification/acceptance-ui/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "scripts": { + "test:arch": "vitest run --config vitest.arch.config.ts", "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/dom/register-asset-loader --import @cellix/serenity-framework/dom/setup' cucumber-js", "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/dom/register-asset-loader --import @cellix/serenity-framework/dom/setup' c8 cucumber-js", "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/dom/register-asset-loader --import @cellix/serenity-framework/dom/setup' c8 cucumber-js" @@ -26,7 +27,9 @@ "std-env": "^4.0.0" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-typescript": "workspace:*", + "@cellix/config-vitest": "workspace:*", "@ocom-verification/verification-shared": "workspace:*", "@testing-library/react": "^16.3.0", "@types/node": "catalog:", @@ -34,6 +37,7 @@ "@types/react-dom": "^19.1.6", "c8": "^10.1.3", "tsx": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/ocom-verification/acceptance-ui/src/archunit-tests/serenity-conventions.test.ts b/packages/ocom-verification/acceptance-ui/src/archunit-tests/serenity-conventions.test.ts new file mode 100644 index 000000000..e98032542 --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/archunit-tests/serenity-conventions.test.ts @@ -0,0 +1,5 @@ +import { describeSerenitySuiteConventionTests } from '@cellix/archunit-tests/serenity'; + +describeSerenitySuiteConventionTests({ + suiteRoot: 'src', +}); diff --git a/packages/ocom-verification/acceptance-ui/vitest.arch.config.ts b/packages/ocom-verification/acceptance-ui/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/packages/ocom-verification/e2e-tests/package.json b/packages/ocom-verification/e2e-tests/package.json index 57262c353..4c385c7d6 100644 --- a/packages/ocom-verification/e2e-tests/package.json +++ b/packages/ocom-verification/e2e-tests/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "scripts": { + "test:arch": "vitest run --config vitest.arch.config.ts", "test:e2e": "pnpm run portless:prune && pnpm run proxy:start && pnpm run test:e2e:run", "test:e2e:ci": "pnpm run portless:prune && pnpm run proxy:start:ci && pnpm run test:e2e:run", "test:e2e:run": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", @@ -25,15 +26,18 @@ "std-env": "^4.0.0" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/local-dev": "workspace:*", "@ocom/local-dev-config": "workspace:*", "@ocom/service-queue-storage": "workspace:*", "@cellix/config-typescript": "workspace:*", + "@cellix/config-vitest": "workspace:*", "@ocom-verification/verification-shared": "workspace:*", "@types/node": "catalog:", "playwright": "catalog:", "rimraf": "^6.0.1", "tsx": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/ocom-verification/e2e-tests/src/archunit-tests/serenity-conventions.test.ts b/packages/ocom-verification/e2e-tests/src/archunit-tests/serenity-conventions.test.ts new file mode 100644 index 000000000..0459db109 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/archunit-tests/serenity-conventions.test.ts @@ -0,0 +1,6 @@ +import { describeSerenitySuiteConventionTests } from '@cellix/archunit-tests/serenity'; + +describeSerenitySuiteConventionTests({ + suiteRoot: 'src', + requireManagedCleanup: true, +}); diff --git a/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts index f97c38705..99aaf7c3f 100644 --- a/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts @@ -14,6 +14,9 @@ export function registerLifecycleHooks(): void { registerWorldLifecycleHooks({ beforeTimeout: getTimeout('serverStartup') + getTimeout('uiInit') * 3, scenarioTimeout: getTimeout('scenario'), + // The first Before cold-boots all servers + the browser, which exceeds the + // per-scenario budget on a cold machine; give it a dedicated boot timeout. + beforeTimeout: getTimeout('boot'), before: async (world) => { await world.init(); }, diff --git a/packages/ocom-verification/e2e-tests/vitest.arch.config.ts b/packages/ocom-verification/e2e-tests/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/packages/ocom-verification/e2e-tests/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/packages/ocom/service-otel/src/otel-builder.ts b/packages/ocom/service-otel/src/otel-builder.ts index cd10147dc..f21686e57 100644 --- a/packages/ocom/service-otel/src/otel-builder.ts +++ b/packages/ocom/service-otel/src/otel-builder.ts @@ -1,16 +1,16 @@ -import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node'; -import { AzureMonitorTraceExporter, AzureMonitorMetricExporter, AzureMonitorLogExporter } from '@azure/monitor-opentelemetry-exporter'; -import { BatchLogRecordProcessor, SimpleLogRecordProcessor, ConsoleLogRecordExporter } from '@opentelemetry/sdk-logs'; -import { BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter, type Sampler, SamplingDecision, ParentBasedSampler, AlwaysOnSampler } from '@opentelemetry/sdk-trace-node'; -import { PeriodicExportingMetricReader, ConsoleMetricExporter } from '@opentelemetry/sdk-metrics'; -import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import azureOtel from '@azure/functions-opentelemetry-instrumentation'; +import { AzureMonitorLogExporter, AzureMonitorMetricExporter, AzureMonitorTraceExporter } from '@azure/monitor-opentelemetry-exporter'; +import type { Attributes, Context, Link, SpanKind } from '@opentelemetry/api'; import { DataloaderInstrumentation } from '@opentelemetry/instrumentation-dataloader'; import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; -import azureOtel from '@azure/functions-opentelemetry-instrumentation'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; - +import { BatchLogRecordProcessor, ConsoleLogRecordExporter, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs'; +import { ConsoleMetricExporter, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; +import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node'; +import { AlwaysOnSampler, BatchSpanProcessor, ConsoleSpanExporter, ParentBasedSampler, type Sampler, SamplingDecision, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node'; import { httpInstrumentationConfig } from './http-config.ts'; -import type { Attributes, Context, Link, SpanKind } from '@opentelemetry/api'; + const { AzureFunctionsInstrumentation } = azureOtel; //Need to use destructuring to access AzureFunctionsInstrumentation (CommonJS module) interface Exporters { diff --git a/packages/ocom/service-queue-storage/turbo.json b/packages/ocom/service-queue-storage/turbo.json index 5cf61f325..44aa7bf51 100644 --- a/packages/ocom/service-queue-storage/turbo.json +++ b/packages/ocom/service-queue-storage/turbo.json @@ -8,7 +8,7 @@ "outputs": ["src/**/*.schema.generated.ts"] }, "build": { - "dependsOn": ["gen"], + "dependsOn": ["^build", "gen"], "inputs": ["$TURBO_EXTENDS$"], "outputs": ["$TURBO_EXTENDS$"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fb196c84..35c407400 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,11 +132,12 @@ overrides: jiti: 2.6.1 rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 - brace-expansion: 5.0.7 + brace-expansion@1: 2.1.1 + brace-expansion@5: 5.0.7 diff@4.0.2: 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 - protobufjs@7.5.4: 7.6.3 + protobufjs@7.5.4: 7.6.5 serve-handler>minimatch: 3.1.5 serialize-javascript@6.0.2: 7.0.5 serialize-javascript@7.0.4: 7.0.5 @@ -153,7 +154,7 @@ overrides: node-forge@<1.3.2: '>=1.3.2' picomatch: ^4.0.4 webpack: ^5.105.4 - webpack-dev-server: ^5.2.6 + webpack-dev-server: 5.2.6 express-rate-limit: 8.5.1 '@azure/ms-rest-js>uuid': ^3.4.0 azurite>uuid: ^3.4.0 @@ -161,12 +162,16 @@ overrides: playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 - protobufjs: 7.6.3 + protobufjs: 7.6.5 ip-address: ^10.1.1 fast-uri: ^4.0.1 '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 - js-yaml@4.1.1: 4.2.0 + js-yaml@3.14.2: 3.15.0 + js-yaml@4.1.1: 4.3.0 + js-yaml@4.2.0: 4.3.0 + body-parser@1.20.5: 1.20.6 + body-parser@2.2.2: 2.3.0 shell-quote@<1.8.4: 1.8.4 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 '@opentelemetry/core@2.7.1': 2.8.0 @@ -176,7 +181,7 @@ overrides: form-data: ^4.0.6 webpack-dev-server>http-proxy-middleware: 3.0.7 joi: ^17.13.4 - brace-expansion@2: 2.0.3 + brace-expansion@2: 2.1.1 dottie: 2.0.7 http-proxy-middleware: 3.0.7 immutable: 3.8.3 @@ -333,6 +338,9 @@ importers: specifier: 1.9.0 version: 1.9.0 devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../packages/cellix/archunit-tests '@cellix/config-rolldown': specifier: workspace:* version: link:../../packages/cellix/config-rolldown @@ -559,6 +567,9 @@ importers: specifier: 'catalog:' version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../packages/cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../packages/cellix/config-typescript @@ -692,6 +703,9 @@ importers: specifier: 'catalog:' version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../packages/cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../packages/cellix/config-typescript @@ -1317,9 +1331,15 @@ importers: specifier: ^4.0.0 version: 4.0.0 devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../cellix/config-typescript + '@cellix/config-vitest': + specifier: workspace:* + version: link:../../cellix/config-vitest '@cellix/service-blob-storage': specifier: workspace:* version: link:../../cellix/service-blob-storage @@ -1374,6 +1394,9 @@ importers: typescript: specifier: 'catalog:' version: 6.0.3 + vitest: + specifier: 'catalog:' + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-ui: dependencies: @@ -1420,9 +1443,15 @@ importers: specifier: ^4.0.0 version: 4.0.0 devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../cellix/config-typescript + '@cellix/config-vitest': + specifier: workspace:* + version: link:../../cellix/config-vitest '@ocom-verification/verification-shared': specifier: workspace:* version: link:../verification-shared @@ -1447,6 +1476,9 @@ importers: typescript: specifier: 'catalog:' version: 6.0.3 + vitest: + specifier: 'catalog:' + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/archunit-tests: devDependencies: @@ -1496,9 +1528,15 @@ importers: specifier: ^4.0.0 version: 4.0.0 devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../cellix/config-typescript + '@cellix/config-vitest': + specifier: workspace:* + version: link:../../cellix/config-vitest '@cellix/local-dev': specifier: workspace:* version: link:../../cellix/local-dev @@ -1526,6 +1564,9 @@ importers: typescript: specifier: 'catalog:' version: 6.0.3 + vitest: + specifier: 'catalog:' + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/verification-shared: dependencies: @@ -5520,8 +5561,8 @@ packages: resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.38.0': - resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + '@opentelemetry/semantic-conventions@1.42.0': + resolution: {integrity: sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw==} engines: {node: '>=14'} '@oxc-parser/binding-android-arm-eabi@0.121.0': @@ -6175,9 +6216,6 @@ packages: '@protobufjs/inquire@1.1.1': resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} - '@protobufjs/inquire@1.1.2': - resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} - '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -7875,12 +7913,12 @@ packages: bn.js@5.2.3: resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} - body-parser@1.20.5: - resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + body-parser@1.20.6: + resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} bonjour-service@1.3.0: @@ -7901,8 +7939,8 @@ packages: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} - brace-expansion@2.0.3: - resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} brace-expansion@5.0.7: resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} @@ -8379,6 +8417,10 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + continuation-local-storage@3.2.1: resolution: {integrity: sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==} @@ -8943,6 +8985,9 @@ packages: es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -9746,6 +9791,10 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -10200,12 +10249,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + js-yaml@3.15.0: + resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} hasBin: true - js-yaml@4.2.0: - resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true jsbi@4.3.2: @@ -12048,8 +12097,8 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - protobufjs@7.6.3: - resolution: {integrity: sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==} + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -12615,11 +12664,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -13436,9 +13480,9 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} @@ -14445,7 +14489,7 @@ snapshots: '@apollo/utils.withrequired': 3.0.0 '@graphql-tools/schema': 10.0.30(graphql@16.12.0) async-retry: 1.3.3 - body-parser: 2.2.2(supports-color@8.1.1) + body-parser: 2.3.0(supports-color@8.1.1) content-type: 1.0.5 cors: 2.8.5 finalhandler: 2.1.1(supports-color@8.1.1) @@ -14745,7 +14789,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -16487,7 +16531,7 @@ snapshots: '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.2 - js-yaml: 4.2.0 + js-yaml: 4.3.0 lodash: 4.18.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -16933,7 +16977,7 @@ snapshots: '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) fs-extra: 11.3.2 joi: 17.13.4 - js-yaml: 4.2.0 + js-yaml: 4.3.0 lodash: 4.18.1 tslib: 2.8.1 transitivePeerDependencies: @@ -16958,7 +17002,7 @@ snapshots: globby: 11.1.0 gray-matter: 4.0.3 jiti: 2.6.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 lodash: 4.18.1 micromatch: 4.0.8 p-queue: 6.6.2 @@ -17534,7 +17578,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 jose: 5.10.0 - js-yaml: 4.2.0 + js-yaml: 4.3.0 lodash: 4.18.1 scuid: 1.1.0 tslib: 2.8.1 @@ -17631,7 +17675,7 @@ snapshots: dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.6.3 + protobufjs: 7.6.5 yargs: 17.7.2 '@hapi/hoek@9.3.0': {} @@ -17869,12 +17913,12 @@ snapshots: '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: @@ -17943,7 +17987,7 @@ snapshots: '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: @@ -18002,7 +18046,7 @@ snapshots: '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 - semver: 7.7.3 + semver: 7.8.0 transitivePeerDependencies: - supports-color @@ -18033,7 +18077,7 @@ snapshots: '@types/shimmer': 1.2.0 import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 - semver: 7.7.4 + semver: 7.8.0 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -18045,7 +18089,7 @@ snapshots: '@types/shimmer': 1.2.0 import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 - semver: 7.7.4 + semver: 7.8.0 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -18073,7 +18117,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - protobufjs: 7.6.3 + protobufjs: 7.6.5 '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': dependencies: @@ -18095,13 +18139,13 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0)': dependencies: @@ -18160,7 +18204,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0)': dependencies: @@ -18170,7 +18214,7 @@ snapshots: '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - semver: 7.7.4 + semver: 7.8.0 '@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0)': dependencies: @@ -18182,7 +18226,7 @@ snapshots: '@opentelemetry/semantic-conventions@1.28.0': {} - '@opentelemetry/semantic-conventions@1.38.0': {} + '@opentelemetry/semantic-conventions@1.42.0': {} '@oxc-parser/binding-android-arm-eabi@0.121.0': optional: true @@ -18646,8 +18690,6 @@ snapshots: '@protobufjs/inquire@1.1.1': {} - '@protobufjs/inquire@1.1.2': {} - '@protobufjs/path@1.1.2': {} '@protobufjs/pool@1.1.0': {} @@ -20625,7 +20667,7 @@ snapshots: bn.js@5.2.3: {} - body-parser@1.20.5: + body-parser@1.20.6: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -20642,17 +20684,17 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.2.2(supports-color@8.1.1): + body-parser@2.3.0(supports-color@8.1.1): dependencies: bytes: 3.1.2 - content-type: 1.0.5 + content-type: 2.0.0 debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.3 on-finished: 2.4.1 qs: 6.15.2 raw-body: 3.0.2 - type-is: 2.0.1 + type-is: 2.1.0 transitivePeerDependencies: - supports-color @@ -20687,7 +20729,7 @@ snapshots: widest-line: 4.0.1 wrap-ansi: 8.1.0 - brace-expansion@2.0.3: + brace-expansion@2.1.1: dependencies: balanced-match: 1.0.2 @@ -20986,7 +21028,7 @@ snapshots: chromatic@16.10.0: dependencies: - semver: 7.7.4 + semver: 7.8.0 chrome-devtools-mcp@1.3.0: {} @@ -21192,6 +21234,8 @@ snapshots: content-type@1.0.5: {} + content-type@2.0.0: {} + continuation-local-storage@3.2.1: dependencies: async-listener: 0.6.10 @@ -21237,7 +21281,7 @@ snapshots: cosmiconfig@8.3.6(typescript@6.0.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -21327,7 +21371,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.10) postcss-modules-values: 4.0.0(postcss@8.5.10) postcss-value-parser: 4.2.0 - semver: 7.7.4 + semver: 7.8.0 optionalDependencies: webpack: 5.105.4(esbuild@0.28.1) @@ -21821,6 +21865,8 @@ snapshots: es-module-lexer@2.0.0: {} + es-module-lexer@2.3.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -22019,7 +22065,7 @@ snapshots: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.5 + body-parser: 1.20.6 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 @@ -22401,7 +22447,7 @@ snapshots: es6-error: 4.1.1 matcher: 3.0.0 roarr: 2.15.4 - semver: 7.7.4 + semver: 7.8.0 serialize-error: 7.0.1 global-dirs@3.0.1: @@ -22526,7 +22572,7 @@ snapshots: gray-matter@4.0.3: dependencies: - js-yaml: 3.14.2 + js-yaml: 3.15.0 kind-of: 6.0.3 section-matter: 1.0.0 strip-bom-string: 1.0.0 @@ -22877,6 +22923,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + icss-utils@5.1.0(postcss@8.5.10): dependencies: postcss: 8.5.10 @@ -23283,12 +23333,12 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.2: + js-yaml@3.15.0: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.2.0: + js-yaml@4.3.0: dependencies: argparse: 2.0.1 @@ -23416,7 +23466,7 @@ snapshots: fast-glob: 3.3.3 formatly: 0.3.0 jiti: 2.6.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 minimist: 1.2.8 oxc-resolver: 11.14.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) picocolors: 1.1.1 @@ -23675,7 +23725,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.0 map-cache@0.2.2: {} @@ -24271,11 +24321,11 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 2.1.1 minimatch@9.0.9: dependencies: - brace-expansion: 2.0.3 + brace-expansion: 2.1.1 minimist@1.2.8: {} @@ -24306,7 +24356,7 @@ snapshots: https-proxy-agent: 7.0.6 mongodb: 6.21.0 new-find-package-json: 2.0.0(supports-color@8.1.1) - semver: 7.7.4 + semver: 7.8.0 tar-stream: 3.1.7 tslib: 2.8.1 yauzl: 3.2.1 @@ -24332,7 +24382,7 @@ snapshots: https-proxy-agent: 7.0.6 mongodb: 6.21.0 new-find-package-json: 2.0.0(supports-color@8.1.1) - semver: 7.7.4 + semver: 7.8.0 tar-stream: 3.1.7 tslib: 2.8.1 yauzl: 3.2.1 @@ -25234,7 +25284,7 @@ snapshots: cosmiconfig: 8.3.6(typescript@6.0.3) jiti: 2.6.1 postcss: 8.5.10 - semver: 7.7.4 + semver: 7.8.0 webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - typescript @@ -25585,7 +25635,7 @@ snapshots: proto-list@1.2.4: {} - protobufjs@7.6.3: + protobufjs@7.6.5: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -25593,7 +25643,6 @@ snapshots: '@protobufjs/eventemitter': 1.1.1 '@protobufjs/fetch': 1.1.1 '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.2 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.1 @@ -26286,7 +26335,7 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.0 semver@5.7.2: {} @@ -26294,8 +26343,6 @@ snapshots: semver@7.7.2: {} - semver@7.7.3: {} - semver@7.7.4: {} semver@7.8.0: {} @@ -27162,9 +27209,9 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - type-is@2.0.1: + type-is@2.1.0: dependencies: - content-type: 1.0.5 + content-type: 2.0.0 media-typer: 1.1.0 mime-types: 3.0.2 @@ -27320,7 +27367,7 @@ snapshots: is-yarn-global: 0.4.1 latest-version: 7.0.0 pupa: 3.3.0 - semver: 7.7.4 + semver: 7.8.0 semver-diff: 4.0.0 xdg-basedir: 5.1.0 @@ -27661,7 +27708,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.20.0 - es-module-lexer: 2.0.0 + es-module-lexer: 2.3.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b48bb6c08..317aa125e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -80,11 +80,12 @@ overrides: jiti: 2.6.1 rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 - brace-expansion: 5.0.7 + 'brace-expansion@1': 2.1.1 + 'brace-expansion@5': 5.0.7 'diff@4.0.2': 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 - 'protobufjs@7.5.4': 7.6.3 + 'protobufjs@7.5.4': 7.6.5 'serve-handler>minimatch': 3.1.5 'serialize-javascript@6.0.2': 7.0.5 'serialize-javascript@7.0.4': 7.0.5 @@ -101,7 +102,7 @@ overrides: node-forge@<1.3.2: '>=1.3.2' picomatch: ^4.0.4 webpack: ^5.105.4 - webpack-dev-server: ^5.2.6 + webpack-dev-server: 5.2.6 express-rate-limit: 8.5.1 '@azure/ms-rest-js>uuid': '^3.4.0' 'azurite>uuid': '^3.4.0' @@ -109,12 +110,16 @@ overrides: playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 - protobufjs: 7.6.3 + protobufjs: 7.6.5 ip-address: ^10.1.1 fast-uri: ^4.0.1 '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 - 'js-yaml@4.1.1': 4.2.0 + 'js-yaml@3.14.2': 3.15.0 + 'js-yaml@4.1.1': 4.3.0 + 'js-yaml@4.2.0': 4.3.0 + 'body-parser@1.20.5': 1.20.6 + 'body-parser@2.2.2': 2.3.0 'shell-quote@<1.8.4': 1.8.4 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 '@opentelemetry/core@2.7.1': 2.8.0 @@ -124,7 +129,7 @@ overrides: form-data: ^4.0.6 'webpack-dev-server>http-proxy-middleware': 3.0.7 joi: ^17.13.4 - brace-expansion@2: 2.0.3 + brace-expansion@2: 2.1.1 dottie: 2.0.7 http-proxy-middleware: 3.0.7 immutable: 3.8.3 diff --git a/readme.md b/readme.md index 570439ab0..d15f9a91c 100644 --- a/readme.md +++ b/readme.md @@ -147,6 +147,8 @@ pnpm run build ``` + + Mock OIDC server The local mock OpenID Connect provider used for UI development is configured per-portal via JSON files placed in the UI app directories. @@ -215,6 +217,8 @@ First-time setup: configure TLS trust for portless local domains. See ADR 0028 ## Turborepo Optimization + + This monorepo uses [Turborepo](https://turbo.build/) for optimized builds and testing: - **Selective Builds**: Only affected packages are built/tested in CI