@ingenioz-it/caits 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/bin/caits.js +17 -12
  2. package/bin/init.js +15 -6
  3. package/package.json +1 -1
  4. package/quality/architecture/adapters/marko.mjs +7 -4
  5. package/quality/architecture/adapters/scss.mjs +29 -8
  6. package/quality/architecture/adapters/typescript.mjs +2 -2
  7. package/quality/architecture/facts.mjs +137 -30
  8. package/quality/architecture/project.mjs +114 -22
  9. package/quality/architecture.mjs +68 -22
  10. package/quality/check.mjs +20 -14
  11. package/quality/dev.mjs +3 -2
  12. package/quality/mutation-suite.mjs +56 -5
  13. package/quality/mutation-summary.mjs +28 -11
  14. package/quality/mutation.mjs +5 -6
  15. package/quality/terminal.mjs +30 -0
  16. package/quality/test-docs/test-docs.mjs +1 -2
  17. package/src/DomainEvent.ts +4 -1
  18. package/src/di/Container.ts +5 -1
  19. package/src/di/createBackend.ts +51 -8
  20. package/src/di/createFrontend.ts +7 -4
  21. package/src/di/discover.ts +35 -6
  22. package/src/di/discoverFrontends.ts +29 -6
  23. package/src/di/discovered.ts +3 -3
  24. package/src/di/discoveredFrontends.ts +3 -3
  25. package/src/di/frontend.ts +46 -4
  26. package/src/messaging/BackendEventBus.ts +4 -3
  27. package/src/messaging/ContextMap.ts +28 -4
  28. package/src/messaging/EventPublisher.ts +2 -2
  29. package/src/messaging/FrontendContextMap.ts +33 -0
  30. package/src/testing/backend/applications.ts +7 -6
  31. package/src/testing/backend/context-map.ts +4 -1
  32. package/src/testing/frontend/context-map.ts +6 -0
  33. package/src/testing/frontend/frontends.ts +16 -9
  34. package/src/virtual-modules.d.ts +3 -1
  35. package/src/vite.js +20 -13
  36. package/template/caits/architecture.mjs +1000 -177
  37. package/template/caits/eslint.js +18 -0
  38. package/template/caits/imports.mjs +14 -0
  39. package/template/caits/tsconfig.json +1 -1
  40. package/template/caits/vite.ts +4 -2
  41. package/template/project/AGENTS +9 -5
  42. package/template/project/quality/imports.mjs +10 -0
@@ -0,0 +1,33 @@
1
+ type EventClass<E> = abstract new (...args: never[]) => E;
2
+
3
+ /**
4
+ * One line of the context map of a frontend context: an event of the context shared that it hears, which becomes events of the context,
5
+ * or an event of the context that it shares, which becomes events of the context shared.
6
+ */
7
+ export type Line = { readonly shares: boolean; readonly event: EventClass<object>; readonly events: (event: never) => object[] };
8
+
9
+ /**
10
+ * A line that hears the context shared, in the context map of a frontend context (its context-map.ts): when this event of shared comes
11
+ * on the shared bus, the context publishes these events of its own on its own bus, and its store reads them. A context map names only
12
+ * its own context and the context shared: two contexts never name each other.
13
+ */
14
+ export function when<E extends object>(event: EventClass<E>, events: (event: E) => object[]): Line {
15
+ return { shares: false, event, events };
16
+ }
17
+
18
+ /**
19
+ * A line that tells the other contexts: when a view, a component or a handler of the context publishes this event of its own on its
20
+ * bus, the context publishes these events of shared on the shared bus, and the context map of each context that hears them turns them
21
+ * into events of its own. The event of the context holds what the events of shared need.
22
+ */
23
+ export function share<E extends object>(event: EventClass<E>, events: (event: E) => object[]): Line {
24
+ return { shares: true, event, events };
25
+ }
26
+
27
+ /**
28
+ * The events that a map gives for an event, line by line, none when no line reads it: for an event of shared that it hears, events of
29
+ * the context; for an event of the context that it shares, events of shared.
30
+ */
31
+ export function eventsFor(map: readonly Line[], event: object): object[] {
32
+ return map.filter(line => event instanceof line.event).flatMap(line => line.events(event as never));
33
+ }
@@ -14,7 +14,7 @@ const testBindings: Record<string, readonly Binding[]> = Object.fromEntries(
14
14
 
15
15
  type EventType<E extends DomainEvent> = abstract new (...args: never[]) => E;
16
16
 
17
- /** The whole backend, built from the bindings of its contexts with their test.di.ts over them, and predictable ids. For the tests of the context map. */
17
+ /** The whole backend, built from the bindings of its contexts with their test.di.ts over them, and predictable ids. For the tests of the context maps. */
18
18
  export function createTestBackendApplication() {
19
19
  const reportedErrors: unknown[] = [];
20
20
  const published: Published[] = [];
@@ -29,7 +29,7 @@ export function createTestBackendApplication() {
29
29
  return {
30
30
  ...backend,
31
31
  reportedErrors,
32
- /** Every event the contexts published, in its envelope, in the order they were published. */
32
+ /** Every event of shared that the context maps shared, in its envelope, in the order they were published. */
33
33
  published,
34
34
  publishedNames: () => published.map(({ event }) => event.constructor.name),
35
35
  publishedOf: <E extends DomainEvent>(type: EventType<E>): E[] => published.flatMap(({ event }) => event instanceof type ? [event] : [])
@@ -37,13 +37,14 @@ export function createTestBackendApplication() {
37
37
  }
38
38
 
39
39
  /**
40
- * Every context, built from its di.ts with its test.di.ts over it, but alone: no event reaches another context. A test sends input DTOs
41
- * and reads the events the handlers published.
40
+ * Every context, built from its di.ts with its test.di.ts over it, but alone: no context map hears or shares an event. A test sends
41
+ * input DTOs and reads the events of their context that the handlers published.
42
42
  */
43
43
  export function createTestApplication() {
44
44
  const published: DomainEvent[] = [];
45
- const backend = createBackend({ ...discovered, map: [] }, { replace: testBindings, reportError: () => undefined });
46
- backend.subscribe(({ event }) => { published.push(event); });
45
+ const alone = Object.fromEntries(Object.entries(discovered.contexts).map(([name, context]) => [name, { ...context, map: [] }]));
46
+ const backend = createBackend({ ...discovered, contexts: alone }, { replace: testBindings, reportError: () => undefined });
47
+ backend.subscribeToContexts(event => { published.push(event); });
47
48
 
48
49
  return {
49
50
  /** Sends an input DTO to the message bus of the context that handles it. */
@@ -1,7 +1,10 @@
1
1
  import { test as base } from "vitest";
2
2
  import { createTestBackendApplication, type TestBackendApplication } from "./applications.js";
3
3
 
4
- /** Everything a test of the context map imports. `test` checks the Input DTOs that the `ContextMap` gives for an Event. */
4
+ /**
5
+ * Everything a test of the context map imports. `test` checks what the `ContextMap` gives: the input DTOs for an event of shared
6
+ * (`inputsFor`), the events of shared for an event of the context (`eventsFor`).
7
+ */
5
8
  export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
6
9
  export { ContextMap } from "../../messaging/ContextMap.js";
7
10
  export type { TestBackendApplication } from "./applications.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Everything a test of the context map of a frontend context imports: `eventsFor` gives the events of the context that an event of the
3
+ * context shared gives, and the events of shared that an event of the context gives.
4
+ */
5
+ export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
6
+ export { eventsFor } from "../../messaging/FrontendContextMap.js";
@@ -1,23 +1,30 @@
1
1
  import { FrontendEventBus } from "../../messaging/FrontendEventBus.js";
2
2
  import type { Input } from "../../messaging/Input.js";
3
3
  import { createStore, type StoreDefinition } from "../../state/createStore.js";
4
- import type { Frontend } from "../../state/request.js";
4
+ import type { EventPublisher } from "../../messaging/EventPublisher.js";
5
5
  import { discoveredFrontends } from "../../di/discoveredFrontends.js";
6
6
  import { frontendsOn, type Frontends } from "../../di/frontend.js";
7
7
 
8
- type StateOf<F> = F extends Frontend<infer S, unknown> ? S : never;
9
- type EventOf<F> = F extends Frontend<unknown, infer E> ? E : never;
8
+ type EventOf<F> = F extends { events: EventPublisher<infer E> } ? E : never;
9
+ type StoreOf<F> = F extends { store: infer S } ? S : undefined;
10
+
11
+ /** The presentation of a context in a test: its frontend, the DTOs that its store sent, and the events that it published. */
12
+ export type TestPresentation<F> = { events: EventPublisher<EventOf<F>>; store: StoreOf<F>; sent: Input<unknown>[]; published: EventOf<F>[] };
10
13
 
11
14
  /**
12
- * The presentation of a context, as its views get it: its EventPublisher and its store. The message bus of the store is a Fake: it
13
- * records the DTOs it receives, and answers nothing. A test publishes the events of the handlers itself.
15
+ * The presentation of a context, as its views and its components get it: its EventPublisher and its store (none, for a context
16
+ * without a store). The message bus of the store is a Fake: it records the DTOs it receives in `sent`, and answers nothing. A test
17
+ * publishes the events of the handlers itself. `published` records every event of the bus, in their order: those that the views and
18
+ * the components publish. The context map is left aside: its own test reads what it shares.
14
19
  */
15
- export function testPresentation<Name extends keyof Frontends>(name: Name): Frontend<StateOf<Frontends[Name]>, EventOf<Frontends[Name]>> & { sent: Input<unknown>[] } {
20
+ export function testPresentation<Name extends keyof Frontends>(name: Name): TestPresentation<Frontends[Name]> {
16
21
  const events = new FrontendEventBus<EventOf<Frontends[Name]>>();
17
22
  const sent: Input<unknown>[] = [];
18
- const definition = discoveredFrontends[name].store as StoreDefinition<StateOf<Frontends[Name]>, EventOf<Frontends[Name]>>;
19
- const store = createStore(definition, { events, bus: { send: <Output>(input: Input<Output>) => { sent.push(input); return Promise.resolve(undefined as Output); } } });
20
- return { events, store, sent };
23
+ const published: EventOf<Frontends[Name]>[] = [];
24
+ events.subscribe(event => { published.push(event); });
25
+ const definition = discoveredFrontends[name].store as StoreDefinition<unknown, EventOf<Frontends[Name]>> | undefined;
26
+ const store = definition && createStore(definition, { events, bus: { send: <Output>(input: Input<Output>) => { sent.push(input); return Promise.resolve(undefined as Output); } } });
27
+ return { events, store, sent, published } as unknown as TestPresentation<Frontends[Name]>;
21
28
  }
22
29
 
23
30
  /** A $global as Marko gives it to the views: the address of the page, and frontends already built. */
@@ -7,7 +7,7 @@ declare module "virtual:kernel/backend" {
7
7
  export const dtos: Modules;
8
8
  export const handlers: Modules;
9
9
  export const controllers: Modules;
10
- export const map: Record<string, Translation[]>;
10
+ export const maps: Record<string, Translation[]>;
11
11
  }
12
12
 
13
13
  declare module "virtual:kernel/backend-test" {
@@ -17,12 +17,14 @@ declare module "virtual:kernel/backend-test" {
17
17
 
18
18
  declare module "virtual:kernel/frontend" {
19
19
  import type { Binding } from "@ingenioz-it/caits/di/Container";
20
+ import type { Line } from "@ingenioz-it/caits/messaging/FrontendContextMap";
20
21
  import type { StoreDefinition } from "@ingenioz-it/caits/state/createStore";
21
22
  type Modules = Record<string, Record<string, unknown>>;
22
23
  export const bindings: Record<string, readonly Binding[]>;
23
24
  export const dtos: Modules;
24
25
  export const handlers: Modules;
25
26
  export const stores: Record<string, StoreDefinition<unknown, unknown>>;
27
+ export const maps: Record<string, readonly Line[]>;
26
28
  }
27
29
 
28
30
  declare module "virtual:kernel/frontend-test" {
package/src/vite.js CHANGED
@@ -58,17 +58,18 @@ function dependencies(source) {
58
58
  const CONTEXT_MODULES = {
59
59
  "virtual:kernel/backend": [
60
60
  ["bindings", "backend/*/di.ts", true],
61
- ["dtos", "backend/*/application/*/*/*.dto.ts"],
62
- ["handlers", "backend/*/application/*/*/*.handler.ts"],
63
- ["controllers", "backend/*/presentation/*/*.controller.ts"],
64
- ["map", "backend/context-map.ts", true]
61
+ ["dtos", "backend/*/{command,query}/*/*.dto.ts"],
62
+ ["handlers", "backend/*/{command,query}/*/*.handler.ts"],
63
+ ["controllers", "backend/*/{command,query}/*/*.controller.ts"],
64
+ ["maps", "backend/*/context-map.ts", true]
65
65
  ],
66
66
  "virtual:kernel/backend-test": [["bindings", "backend/*/test.di.ts", true]],
67
67
  "virtual:kernel/frontend": [
68
68
  ["bindings", "frontend/*/di.ts", true],
69
- ["dtos", "frontend/*/application/*/*/*.dto.ts"],
70
- ["handlers", "frontend/*/application/*/*/*.handler.ts"],
71
- ["stores", "frontend/*/presentation/store/*Store.ts", true]
69
+ ["dtos", "frontend/*/{command,query}/*/*.dto.ts"],
70
+ ["handlers", "frontend/*/{command,query}/*/*.handler.ts"],
71
+ ["stores", "frontend/*/presentation/store/*Store.ts", true],
72
+ ["maps", "frontend/*/context-map.ts", true]
72
73
  ],
73
74
  "virtual:kernel/frontend-test": [["bindings", "frontend/*/test.di.ts", true]]
74
75
  };
@@ -86,9 +87,9 @@ function contexts(source) {
86
87
  /** @type {(pattern: string, onlyDefault?: boolean) => string} */
87
88
  const glob = (pattern, onlyDefault = false) =>
88
89
  `import.meta.glob(${JSON.stringify(`${source}/${pattern}`)}, { eager: true${onlyDefault ? `, import: "default"` : ""} })`;
89
- /** A pattern as a regular expression: `*` is a part of a name, without "/". */
90
- const matcherOf = (/** @type {string} */ pattern) =>
91
- new RegExp(`^${`${source}/${pattern}`.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", "[^/]*")}$`);
90
+ /** A pattern as a regular expression: `*` is a part of a name, without "/", and `{command,query}` one of these names. */
91
+ const matcherOf = (/** @type {string} */ pattern) => new RegExp(`^${`${source}/${pattern}`.replace(/[.+?^$()|[\]\\]/g, "\\$&").replaceAll("*", "[^/]*")
92
+ .replace(/\{([^}]*)\}/g, (/** @type {string} */ _, /** @type {string} */ names) => `(?:${names.split(",").join("|")})`)}$`);
92
93
  const modules = Object.entries(CONTEXT_MODULES).map(([id, exports]) => ({
93
94
  id,
94
95
  code: exports.map(([name, pattern, onlyDefault]) => `export const ${name} = ${glob(pattern, onlyDefault)};`).join("\n"),
@@ -102,9 +103,15 @@ function contexts(source) {
102
103
  // Vite processes the package as the code of the project, also when it is installed: it holds TypeScript and imports these modules.
103
104
  ssr: { noExternal: [KERNEL] },
104
105
  optimizeDeps: { exclude: [KERNEL] },
105
- // The shortcut #shared leads to the backend context shared from every file (the tsconfig.json of the project says it to TypeScript).
106
- // Vite reads an alias before the "imports" of the package.json of a context (#context), which cannot lead out of the context.
107
- resolve: { alias: [{ find: /^#shared\//, replacement: `${source}/backend/shared/` }] }
106
+ // The shortcut #shared leads to the backend context shared from every file, and #frontend-shared to the frontend context shared
107
+ // (the tsconfig.json of the project says it to TypeScript). Vite reads an alias before the "imports" of the package.json of a
108
+ // context (#context), which cannot lead out of the context.
109
+ resolve: {
110
+ alias: [
111
+ { find: /^#shared\//, replacement: `${source}/backend/shared/` },
112
+ { find: /^#frontend-shared\//, replacement: `${source}/frontend/shared/` }
113
+ ]
114
+ }
108
115
  }),
109
116
  configResolved: config => { root = config.root; },
110
117
  configureServer: server => {