@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.
- package/bin/caits.js +17 -12
- package/bin/init.js +15 -6
- package/package.json +1 -1
- package/quality/architecture/adapters/marko.mjs +7 -4
- package/quality/architecture/adapters/scss.mjs +29 -8
- package/quality/architecture/adapters/typescript.mjs +2 -2
- package/quality/architecture/facts.mjs +137 -30
- package/quality/architecture/project.mjs +114 -22
- package/quality/architecture.mjs +68 -22
- package/quality/check.mjs +20 -14
- package/quality/dev.mjs +3 -2
- package/quality/mutation-suite.mjs +56 -5
- package/quality/mutation-summary.mjs +28 -11
- package/quality/mutation.mjs +5 -6
- package/quality/terminal.mjs +30 -0
- package/quality/test-docs/test-docs.mjs +1 -2
- package/src/DomainEvent.ts +4 -1
- package/src/di/Container.ts +5 -1
- package/src/di/createBackend.ts +51 -8
- package/src/di/createFrontend.ts +7 -4
- package/src/di/discover.ts +35 -6
- package/src/di/discoverFrontends.ts +29 -6
- package/src/di/discovered.ts +3 -3
- package/src/di/discoveredFrontends.ts +3 -3
- package/src/di/frontend.ts +46 -4
- package/src/messaging/BackendEventBus.ts +4 -3
- package/src/messaging/ContextMap.ts +28 -4
- package/src/messaging/EventPublisher.ts +2 -2
- package/src/messaging/FrontendContextMap.ts +33 -0
- package/src/testing/backend/applications.ts +7 -6
- package/src/testing/backend/context-map.ts +4 -1
- package/src/testing/frontend/context-map.ts +6 -0
- package/src/testing/frontend/frontends.ts +16 -9
- package/src/virtual-modules.d.ts +3 -1
- package/src/vite.js +20 -13
- package/template/caits/architecture.mjs +1000 -177
- package/template/caits/eslint.js +18 -0
- package/template/caits/imports.mjs +14 -0
- package/template/caits/tsconfig.json +1 -1
- package/template/caits/vite.ts +4 -2
- package/template/project/AGENTS +9 -5
- 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
|
|
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
|
|
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
|
|
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
|
|
46
|
-
backend
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
|
9
|
-
type
|
|
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
|
|
13
|
-
* records the DTOs it receives
|
|
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):
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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. */
|
package/src/virtual-modules.d.ts
CHANGED
|
@@ -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
|
|
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/*/
|
|
62
|
-
["handlers", "backend/*/
|
|
63
|
-
["controllers", "backend/*/
|
|
64
|
-
["
|
|
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/*/
|
|
70
|
-
["handlers", "frontend/*/
|
|
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
|
-
|
|
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
|
|
106
|
-
// Vite reads an alias before the "imports" of the package.json of a
|
|
107
|
-
|
|
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 => {
|