@ingenioz-it/caits 0.6.0 → 0.8.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 (57) hide show
  1. package/bin/caits.js +20 -7
  2. package/bin/init.js +16 -1
  3. package/package.json +8 -5
  4. package/quality/architecture/adapters/scss.mjs +2 -2
  5. package/quality/architecture/facts.mjs +39 -11
  6. package/quality/architecture/project.mjs +8 -6
  7. package/quality/dev.mjs +6 -3
  8. package/quality/mutation-summary.mjs +30 -20
  9. package/quality/mutation.mjs +170 -14
  10. package/src/adapter.js +94 -0
  11. package/src/di/Container.ts +27 -0
  12. package/src/di/RequestContext.ts +8 -2
  13. package/src/di/backend.ts +12 -0
  14. package/src/di/createBackend.ts +197 -25
  15. package/src/di/declareDependencies.js +25 -5
  16. package/src/di/discover.ts +19 -9
  17. package/src/di/discoverFrontends.ts +4 -8
  18. package/src/di/discovered.ts +2 -2
  19. package/src/di/discoveredFrontends.ts +2 -2
  20. package/src/messaging/BackendEventBus.ts +2 -3
  21. package/src/messaging/ContextEvents.ts +11 -0
  22. package/src/messaging/ContextMap.ts +3 -4
  23. package/src/messaging/Controller.ts +36 -75
  24. package/src/messaging/EventSubscriber.ts +1 -1
  25. package/src/messaging/InProcessMessageBus.ts +8 -9
  26. package/src/messaging/MessageBus.ts +4 -2
  27. package/src/messaging/Request.ts +116 -0
  28. package/src/messaging/RequestEvents.ts +8 -0
  29. package/src/messaging/StreamController.ts +261 -0
  30. package/src/messaging/TimerController.ts +25 -0
  31. package/src/messaging/sse.ts +23 -0
  32. package/src/server/entry.ts +37 -0
  33. package/src/server/kernel.ts +19 -0
  34. package/src/server/packages.d.ts +12 -0
  35. package/src/server/serve.js +121 -0
  36. package/src/state/createStore.ts +1 -2
  37. package/src/testing/backend/application.ts +1 -1
  38. package/src/testing/backend/applicationScenario.ts +6 -7
  39. package/src/testing/backend/applications.ts +4 -1
  40. package/src/testing/backend/context-map.ts +1 -1
  41. package/src/testing/backend/integration.ts +1 -1
  42. package/src/testing/backend/presentationApplication.ts +23 -3
  43. package/src/testing/backend/presentationScenario.ts +84 -7
  44. package/src/testing/frontend/applicationScenario.ts +2 -3
  45. package/src/testing/frontend/applications.ts +3 -3
  46. package/src/testing/frontend/frontends.ts +3 -4
  47. package/src/testing/shared/FakeMessageBus.ts +39 -5
  48. package/src/virtual-modules.d.ts +0 -2
  49. package/src/vite.js +2 -2
  50. package/src/vite.types.d.ts +7 -0
  51. package/template/caits/architecture.mjs +162 -113
  52. package/template/caits/imports.mjs +2 -2
  53. package/template/caits/stryker.mjs +11 -6
  54. package/template/caits/vite.ts +15 -5
  55. package/template/project/AGENTS +1 -1
  56. package/quality/mutation-suite.mjs +0 -82
  57. package/src/messaging/Input.ts +0 -4
@@ -5,7 +5,7 @@ import {
5
5
  LISTS, linesOf, locate, portOf, resolveImport, typeOfEvent
6
6
  } from "@ingenioz-it/caits/quality/architecture";
7
7
 
8
- /** @import { Located, Location, Rule } from "@ingenioz-it/caits/quality/architecture" */
8
+ /** @import { File, Located, Location, Rule } from "@ingenioz-it/caits/quality/architecture" */
9
9
 
10
10
  /**
11
11
  * The rules of the architecture, by layer. Each rule has a name and a description, reads one kind of thing (`on`), and checks it:
@@ -55,11 +55,35 @@ const fileKindOf = target =>
55
55
  */
56
56
  const withinContext = (from, to) => from.kind === "layer" && to.kind === "layer" && to.side === from.side && to.layer !== "di"
57
57
  && (to.context === from.context || to.context === "shared");
58
- /** The port of an adapter, from the path of the adapter: its name and its path. @type {(target: string) => { name: string; path: string } | null} */
59
- const portOfAdapter = target => {
60
- const adapter = adapterOf(target);
61
- return adapter && { name: adapter.port, path: `${adapter.portModule}.ts` };
58
+ /**
59
+ * The name of a port: the interface that its module exports, or the name of its file when the module exports none yet.
60
+ * @type {(module: string, files: File[]) => string}
61
+ */
62
+ const portNameOf = (module, files) => files.find(file => file.path === `${module}.ts`)?.exports.find(({ kind }) => kind === "interface")?.name ?? fileName(module);
63
+ /**
64
+ * The port of an adapter, from the path of the adapter: its name and its path, when the domain declares it.
65
+ * @type {(target: string, project: { paths: Set<string>; files: File[] }) => { name: string; path: string } | null}
66
+ */
67
+ const portOfAdapter = (target, { paths, files }) => {
68
+ const module = adapterOf(target, paths)?.portModule;
69
+ return module ? { name: portNameOf(module, files), path: `${module}.ts` } : null;
62
70
  };
71
+ /**
72
+ * The names that a file imports from a module of the project, without its extension: `implements NoteRepository` names the port when
73
+ * NoteRepository comes from the module of the port.
74
+ * @type {(file: File, module: string, root: string) => Set<string>}
75
+ */
76
+ const namesFrom = (file, module, root) => new Set(file.imported
77
+ .filter(({ from }) => resolveImport(file.path, from, root).target.replace(/\.(js|ts)$/, "") === module).map(({ name }) => name));
78
+ /**
79
+ * The port folder of the domain that a class implements, through the module that the file imports its interface from.
80
+ * @type {(file: File, root: string) => { implementer: string; name: string; folder: string } | undefined}
81
+ */
82
+ const implementedPortOf = (file, root) => file.classes.flatMap(declared => declared.implements.flatMap(name => {
83
+ const from = file.imported.find(imported => imported.name === name)?.from;
84
+ const port = from === undefined ? null : portOf(`${resolveImport(file.path, from, root).target.replace(/\.(js|ts)$/, "")}.ts`);
85
+ return port && !port.contract ? [{ implementer: declared.name ?? "Its class", name, folder: port.folder }] : [];
86
+ }))[0];
63
87
  /**
64
88
  * The file that an import of a stylesheet loads: Sass adds the extension, and the _ of a partial.
65
89
  * @type {(target: string, paths: Set<string>) => string}
@@ -138,6 +162,9 @@ function listedPackages(layer) {
138
162
  };
139
163
  }
140
164
 
165
+ /** The controllers of the kernel, by what triggers them, and their module: a request, a timer, a request that a stream answers. */
166
+ const KERNEL_CONTROLLERS = /** @type {const} */ ({ Controller: "messaging/Controller", TimerController: "messaging/TimerController", StreamController: "messaging/StreamController" });
167
+
141
168
  /** @type {Rule[]} */
142
169
  export const rules = [
143
170
  // Kernel: the package @ingenioz-it/caits builds the containers and the controllers
@@ -154,11 +181,11 @@ export const rules = [
154
181
  },
155
182
  {
156
183
  name: "controllers-built-by-kernel",
157
- description: "Only the kernel makes a controller, with its dependencies. The other code asks the kernel for a controller, or sends a request through the router.",
184
+ description: "Only the kernel makes a controller, with its dependencies. The other code asks the kernel for a controller, or sends a request through the router. The AbortController of the platform, which stops a fetch, is not a controller of the app.",
158
185
  on: "file",
159
186
  check: ({ path, constructed, lines }) => {
160
187
  const route = locate(path).kind === "routes";
161
- return constructed.flatMap((name, index) => name === null || !/^[A-Z]\w*Controller$/.test(name) ? [] : [at(lines.constructed[index],
188
+ return constructed.flatMap((name, index) => name === null || name === "AbortController" || !/^[A-Z]\w*Controller$/.test(name) ? [] : [at(lines.constructed[index],
162
189
  `This file builds ${name} with \`new\`, but only the kernel builds a controller, with its dependencies. ${route ? `Ask the kernel for it with \`controller(${name})\`, from @ingenioz-it/caits/di/backend.` : "Send the request through the router instead."}`)]);
163
190
  }
164
191
  },
@@ -171,16 +198,16 @@ export const rules = [
171
198
  on: "file",
172
199
  /** The name of the files of each folder of the domain: what it says, and the name to give. */
173
200
  kinds: {
174
- entity: "the name of an entity is its class, in PascalCase: entity/{Entity}.ts",
175
- "value-object": "the name of a value object is its class, in PascalCase: value-object/{ValueObject}.ts",
176
- error: "the name of an error ends with Error: error/{Name}Error.ts",
177
- event: "the name of a domain event is its class, in PascalCase: event/{DomainEvent}.ts",
201
+ entity: "the file of an entity is in PascalCase: entity/{Entity}.ts",
202
+ "value-object": "the file of a value object is in PascalCase: value-object/{ValueObject}.ts",
203
+ error: "the file of an error is in PascalCase: error/{Error}.ts",
204
+ event: "the file of a domain event is in PascalCase: event/{DomainEvent}.ts",
178
205
  port: "a port is port/{port}/{Port}.ts, beside its contract {Port}.contract.ts"
179
206
  },
180
207
  /** @this {{ kinds: Record<string, string> }} */
181
208
  check({ path }) {
182
209
  if (!/^src\/(backend|frontend)\/[^/]+\/domain\//.test(path)
183
- || /^src\/(backend|frontend)\/[^/]+\/domain\/((entity|value-object)\/[A-Z]\w*(\.test)?|error\/[A-Z]\w*Error|event\/[A-Z]\w*|port\/[a-z0-9-]+\/[A-Z]\w*?(\.contract)?)\.ts$/.test(path)) return;
210
+ || /^src\/(backend|frontend)\/[^/]+\/domain\/((entity|value-object)\/[A-Z]\w*(\.test)?|error\/[A-Z]\w*|event\/[A-Z]\w*|port\/[a-z0-9-]+\/[A-Z]\w*?(\.contract)?)\.ts$/.test(path)) return;
184
211
  const [, context, rest] = /^(src\/(?:backend|frontend)\/[^/]+)\/domain\/(.+)$/.exec(path) ?? [];
185
212
  const name = fileName(path);
186
213
  const parts = rest.split("/");
@@ -198,11 +225,10 @@ export const rules = [
198
225
  if (test && kind !== "entity" && kind !== "value-object") {
199
226
  return `${name} is a test in ${kind}/, but in the domain only the entities and the value objects have a test. Test it through the application test of a use case${kind === "port" ? ", or through the contract of the port" : ""}.`;
200
227
  }
201
- const stem = kind === "error" && !base.endsWith("Error") ? `${base}Error` : base;
202
- const suggested = `${stem}${test ? ".test" : contract ? ".contract" : ""}.ts`;
228
+ const suggested = `${base}${test ? ".test" : contract ? ".contract" : ""}.ts`;
203
229
  const folder = kind === "port" ? `port/${kebab(base)}` : kind;
204
230
  if (parts.slice(0, -1).join("/") !== folder) return `${name} is in ${parts.slice(0, -1).join("/")}/, but its place is ${folder}/. Move it to ${context}/domain/${folder}/${suggested}.`;
205
- return `${name} is in ${folder}/, but ${this.kinds[kind]}. Rename it ${suggested}${kind === "error" ? `, and its class ${stem}` : ""}.`;
231
+ return `${name} is in ${folder}/, but ${this.kinds[kind]}. Rename it ${suggested}.`;
206
232
  }
207
233
  },
208
234
  {
@@ -216,23 +242,22 @@ export const rules = [
216
242
  const names = exports.filter(({ kind }) => this.declarations.has(kind)).map(({ name }) => name);
217
243
  if (names.length === exports.length && new Set(names).size === 1) return;
218
244
  const file = fileName(path);
219
- const expected = file.slice(0, -".ts".length);
220
245
  const unnamed = exports.find(({ kind }) => kind === "default" || kind === "all");
221
246
  if (unnamed) {
222
- return at(unnamed.line, `${file} has an export without a name (${unnamed.kind === "all" ? "export *" : "export default"}), but a domain file exports one name, ${expected}. Export it as ${expected}.`);
247
+ return at(unnamed.line, `${file} has an export without a name (${unnamed.kind === "all" ? "export *" : "export default"}), but a domain file exports one name. Export its declaration with its name: \`export class …\`.`);
223
248
  }
224
249
  const distinct = [...new Set(exports.map(({ name }) => name ?? ""))];
225
250
  if (distinct.length > 1) {
226
- const extras = distinct.filter(name => name !== expected);
227
- const first = exports.find(({ name }) => extras.includes(name ?? "")) ?? exports[0];
251
+ const [kept, ...extras] = distinct;
252
+ const first = exports.find(({ name }) => name === extras[0]) ?? exports[0];
228
253
  const port = /\/domain\/port\//.test(path) ? ", which the port and its token share" : "";
229
- return at(first.line, `${file} exports ${distinct.length} names (${distinct.join(", ")}), but a domain file exports one name, ${expected}${port}. Move ${listOf(extras)} into ${expected} as a static member, or to a file of its own.`);
254
+ return at(first.line, `${file} exports ${distinct.length} names (${distinct.join(", ")}), but a domain file exports one name${port}. Move ${listOf(extras)} into ${kept} as a static member, or to a file of its own.`);
230
255
  }
231
256
  const listed = exports.find(({ kind }) => kind === "specifier");
232
257
  if (listed) {
233
258
  return at(listed.line, `${file} exports ${listed.name} from a list, \`export { … }\`, but a domain file exports its one name where it declares it. Write \`export\` before the declaration of ${listed.name}.`);
234
259
  }
235
- return `${file} exports nothing, but a domain file exports one name, ${expected}. Export ${expected}.`;
260
+ return `${file} exports nothing, but a domain file exports one name. Write \`export\` before its declaration.`;
236
261
  }
237
262
  },
238
263
  {
@@ -240,11 +265,13 @@ export const rules = [
240
265
  description: "An entity or a value object is an exported class. The class holds the business rules of its values.",
241
266
  on: "file",
242
267
  check: ({ path, classes, exports }) => {
243
- const [, kind, name] = /^src\/(?:backend|frontend)\/[^/]+\/domain\/(entity|value-object)\/([A-Z]\w*)\.ts$/.exec(path) ?? [];
268
+ const kind = /^src\/(?:backend|frontend)\/[^/]+\/domain\/(entity|value-object)\/[A-Z]\w*\.ts$/.exec(path)?.[1];
244
269
  if (!kind || classes.some(({ exported }) => exported)) return;
245
- const rule = `${kind === "entity" ? "an entity" : "a value object"} is an exported class, which holds the business rules of its values. Write \`export class ${name}\`, and put the rules of its values in it.`;
246
- const exported = exports.find(entry => entry.name === name);
247
- return exported ? at(exported.line, `${name}.ts exports ${name} as ${kindOf(exported.kind)}, but ${rule}`) : `${name}.ts exports no class, but ${rule}`;
270
+ const rule = `${kind === "entity" ? "an entity" : "a value object"} is an exported class, which holds the business rules of its values.`;
271
+ const [exported] = exports;
272
+ return exported?.name
273
+ ? at(exported.line, `${fileName(path)} exports ${exported.name} as ${kindOf(exported.kind)}, but ${rule} Make it a class, \`export class …\`, and put the rules of its values in it.`)
274
+ : `${fileName(path)} exports no class, but ${rule} Write \`export class …\`, and put the rules of its values in it.`;
248
275
  }
249
276
  },
250
277
  {
@@ -252,17 +279,16 @@ export const rules = [
252
279
  description: "A domain error is an exported class …Error that extends Error.",
253
280
  on: "file",
254
281
  check: ({ path, classes }) => {
255
- const name = /^src\/(?:backend|frontend)\/[^/]+\/domain\/error\/([A-Z]\w*Error)\.ts$/.exec(path)?.[1];
256
- if (!name || classes.some(({ exported, name: declared, extends: parent }) => exported && declared !== null && /^\w+Error$/.test(declared) && parent === "Error")) return;
257
- const fix = `Write \`export class ${name} extends Error\`.`;
258
- const declared = classes.find(({ name: other }) => other === name);
259
- if (!declared) {
260
- const others = classes.flatMap(({ name: other }) => other === null ? [] : [other]);
261
- return `${name}.ts holds no class ${name}${others.length > 0 ? `, only ${listOf(others)}` : ""}, but a domain error is an exported class named after its file, which extends Error. ${fix}`;
262
- }
263
- return at(declared.line, declared.exported
264
- ? `${name} extends ${declared.extends ?? "nothing"}, but a domain error is an exported class that extends Error. ${fix}`
265
- : `${name} is not exported, but a domain error is an exported class that extends Error. ${fix}`);
282
+ if (!/^src\/(?:backend|frontend)\/[^/]+\/domain\/error\/[^/]+\.ts$/.test(path)
283
+ || classes.some(({ exported, name, extends: parent }) => exported && name !== null && /^\w+Error$/.test(name) && parent === "Error")) return;
284
+ const rule = "a domain error is an exported class …Error that extends Error.";
285
+ const declared = classes.find(({ extends: parent }) => parent === "Error") ?? classes.find(({ name }) => name !== null && /Error$/.test(name)) ?? classes[0];
286
+ if (!declared) return `${fileName(path)} holds no class, but ${rule} Write \`export class …Error extends Error\`.`;
287
+ const name = declared.name ?? "Its class";
288
+ const fix = `Write \`export class ${declared.name !== null && /Error$/.test(declared.name) ? declared.name : `${declared.name ?? "…"}Error`} extends Error\`.`;
289
+ if (!declared.exported) return at(declared.line, `${name} is not exported, but ${rule} ${fix}`);
290
+ if (declared.extends !== "Error") return at(declared.line, `${name} extends ${declared.extends ?? "nothing"}, but ${rule} ${fix}`);
291
+ return at(declared.line, `${name} does not end with Error, but ${rule} ${fix}`);
266
292
  }
267
293
  },
268
294
  {
@@ -310,8 +336,8 @@ export const rules = [
310
336
  on: "file",
311
337
  check: (file, { files, paths, root }) => {
312
338
  const port = portOf(file.path);
313
- if (!port?.contract || files.some(other => adapterOf(other.path)?.test && importedModules(other, root).includes(`${port.module}.contract`))) return;
314
- const describe = file.exports.find(({ kind }) => kind === "function")?.name ?? `describe${port.name}Contract`;
339
+ if (!port?.contract || files.some(other => adapterOf(other.path, paths)?.test && importedModules(other, root).includes(`${port.module}.contract`))) return;
340
+ const describe = file.exports.find(({ kind }) => kind === "function")?.name ?? "the function that the contract exports";
315
341
  const infra = `${contextFolderOf(file.path)}/infra/${port.folder}/`;
316
342
  const adapters = [...paths].filter(other => other.startsWith(infra)).map(fileName).sort();
317
343
  if (adapters.length === 0) return `No test runs the contract of ${port.name}, so it checks nothing. Write an adapter of ${port.name} in ${infra}, with a test that calls ${describe}.`;
@@ -341,7 +367,7 @@ export const rules = [
341
367
  },
342
368
  {
343
369
  name: "application-imports-only-listed-packages",
344
- description: "The application (the handlers and the DTOs of the use cases, and the events of the frontend) imports only the packages, the modules of the kernel and the Node APIs that quality/imports.mjs lists for it. CAITS lists those of the domain, @ingenioz-it/caits/messaging/Input and /EventPublisher. The rest is technology: the domain declares a port, the infra implements it, and di.ts binds it.",
370
+ description: "The application (the handlers and the DTOs of the use cases, and the events of the frontend) imports only the packages, the modules of the kernel and the Node APIs that quality/imports.mjs lists for it. CAITS lists those of the domain and @ingenioz-it/caits/messaging/EventPublisher. The rest is technology: the domain declares a port, the infra implements it, and di.ts binds it.",
345
371
  on: "import",
346
372
  /** The fix for a module of the kernel that the list does not hold, by the folder of the module. */
347
373
  kernel: {
@@ -367,10 +393,11 @@ export const rules = [
367
393
  name: "entity-without-port",
368
394
  description: "An entity or a value object does not import a port. It holds the business rules; the handlers call the ports.",
369
395
  on: "import",
370
- check: ({ file, from, target }) => {
371
- const [, kind, name] = /\/domain\/(entity|value-object)\/(?:.+\/)?(\w+)\.ts$/.exec(file.path) ?? [];
396
+ check: ({ file, from, specifier, target }) => {
397
+ const kind = /\/domain\/(entity|value-object)\/(?:.+\/)?\w+\.ts$/.exec(file.path)?.[1];
372
398
  if (from.layer !== "domain" || !kind || !/\/domain\/port\//.test(target)) return;
373
- const port = portOf(target.replace(/\.js$/, ".ts"))?.name ?? fileName(target).replace(/\.(js|ts)$/, "");
399
+ const name = file.classes.find(({ exported }) => exported)?.name ?? fileName(file.path).replace(/\.ts$/, "");
400
+ const port = file.imported.find(({ from: source }) => source === specifier)?.name ?? fileName(target).replace(/\.(js|ts)$/, "");
374
401
  return `The ${kind === "entity" ? "entity" : "value object"} ${name} imports the port ${port} (${target}), but an entity or a value object holds business rules, and never talks to a port. Let the handler call ${port}, and give ${name} the values it needs.`;
375
402
  }
376
403
  },
@@ -421,66 +448,81 @@ export const rules = [
421
448
  },
422
449
  {
423
450
  name: "handler-answers-output",
424
- description: "A backend handler gives its result as an Output DTO: it makes a `new …Output(…)`.",
451
+ description: "A backend handler answers with output DTOs: the return type of its execute names only classes of the DTO file of its use case, one or several (`Promise<AddMessageOutput>`, `Promise<MessageAdded | MessageRefused>`).",
425
452
  on: "file",
426
- check: ({ path, constructed, classes }, { files }) => {
427
- const [, folder, useCase] = /^(src\/backend\/[^/]+\/(?:command|query)\/([^/]+))\/[^/]+\.handler\.ts$/.exec(path) ?? [];
428
- if (!folder || constructed.some(name => name !== null && /^\w+Output$/.test(name))) return;
429
- const handler = classes.find(({ exported }) => exported)?.name ?? `${pascalOf(useCase)}Handler`;
430
- const output = files.find(file => file.path === `${folder}/${useCase}.dto.ts`)?.exports.find(({ name }) => name !== null && /^\w+Output$/.test(name))?.name;
431
- const head = `${handler} builds no Output DTO, but a backend handler answers with one.`;
432
- return output
433
- ? `${head} Return \`new ${output}(…)\` from execute, as ${useCase}.dto.ts declares it.`
434
- : `${head} Declare \`export class ${pascalOf(useCase)}Output\` in ${useCase}.dto.ts, and return \`new ${pascalOf(useCase)}Output(…)\` from execute.`;
453
+ check: ({ path, classes, imported }) => {
454
+ if (!/^src\/backend\/[^/]+\/(?:command|query)\/[^/]+\/[^/]+\.handler\.ts$/.test(path)) return;
455
+ const rule = "a backend handler answers with output DTOs, classes of the DTO file of its use case. Give execute the type of its answers, "
456
+ + "`Promise<AddMessageOutput>`, or `Promise<MessageAdded | MessageRefused>` for two, and declare each class in the DTO file.";
457
+ const execute = classes.flatMap(({ name, methods }) => methods.filter(method => method.name === "execute").map(method => ({ ...method, label: name === null ? "Its execute" : `${name}.execute` })))[0];
458
+ if (!execute) return `${fileName(path)} has no execute, but ${rule}`;
459
+ if (!execute.returns) return at(execute.line, `${execute.label} has no return type, but ${rule}`);
460
+ const dtos = new Set(imported.filter(({ from }) => /\.dto\.(js|ts)$/.test(from)).map(({ name }) => name));
461
+ const others = execute.returns.filter(({ name }) => name === null || !dtos.has(name)).map(({ text }) => `\`${text}\``);
462
+ if (others.length > 0) return at(execute.line, `${execute.label} answers with ${listOf(others)}, ${others.length > 1 ? "which are not DTOs" : "which is not a DTO"} of its use case, but ${rule}`);
435
463
  }
436
464
  },
437
465
  {
438
466
  name: "controller-extends-controller",
439
- description: "A controller is an exported class …Controller that extends the Controller of the kernel.",
467
+ description: "A controller is an exported class …Controller that extends a controller of the kernel: Controller for a request, TimerController for a timer, StreamController for a stream.",
440
468
  on: "file",
441
469
  check: ({ path, classes }) => {
442
- const stem = /^src\/backend\/[^/]+\/(?:command|query)\/[^/]+\/([^/]+)\.controller\.ts$/.exec(path)?.[1];
443
- if (!stem || classes.some(({ exported, name, extends: parent }) => exported && name !== null && /^\w+Controller$/.test(name) && parent === "Controller")) return;
444
- const expected = `${pascalOf(stem)}Controller`;
445
- const rule = `a controller is an exported class …Controller that extends the Controller of the kernel. Write \`export class ${expected} extends Controller\`, with Controller from @ingenioz-it/caits/messaging/Controller.`;
446
- const declared = classes.find(({ name }) => name !== null && /Controller$/.test(name)) ?? classes[0];
470
+ const ofKernel = (/** @type {string | null | undefined} */ parent) => typeof parent === "string" && Object.hasOwn(KERNEL_CONTROLLERS, parent);
471
+ if (!/^src\/backend\/[^/]+\/(?:command|query)\/[^/]+\/[^/]+\.controller\.ts$/.test(path)
472
+ || classes.some(({ exported, name, extends: parent }) => exported && name !== null && /^\w+Controller$/.test(name) && ofKernel(parent))) return;
473
+ const declared = classes.find(({ name }) => name !== null && /Controller$/.test(name)) ?? classes.find(({ extends: parent }) => ofKernel(parent)) ?? classes[0];
474
+ const expected = declared?.name ? (/Controller$/.test(declared.name) ? declared.name : `${declared.name}Controller`) : "…Controller";
475
+ const base = /\.timer\.controller\.ts$/.test(path) ? "TimerController" : /\.stream\.controller\.ts$/.test(path) ? "StreamController" : "Controller";
476
+ const rule = `a controller is an exported class …Controller that extends a controller of the kernel: Controller for a request, TimerController for a timer, StreamController for a stream. Write \`export class ${expected} extends ${base}\`, with ${base} from @ingenioz-it/caits/${KERNEL_CONTROLLERS[base]}.`;
447
477
  if (!declared) return `${fileName(path)} holds no exported class …Controller, but ${rule}`;
448
478
  const name = declared.name ?? "Its class";
449
479
  if (!declared.exported) return at(declared.line, `${name} is not exported, but ${rule}`);
450
- if (declared.extends !== "Controller") return at(declared.line, `${name} extends ${declared.extends ?? "nothing"}, but ${rule}`);
480
+ if (!ofKernel(declared.extends)) return at(declared.line, `${name} extends ${declared.extends ?? "nothing"}, but ${rule}`);
451
481
  return at(declared.line, `${name} does not end with Controller, but ${rule}`);
452
482
  }
453
483
  },
484
+ {
485
+ name: "controllers-read-the-request-of-caits",
486
+ description: "A controller reads the Request of CAITS, from @ingenioz-it/caits/messaging/Request, which gives the content of the body at each call. The Request of the web, global, reads its body once only: a controller neither names it nor builds a request.",
487
+ on: "file",
488
+ check: ({ path, imported, typeReferences, constructed, lines }) => {
489
+ if (!/^src\/backend\/[^/]+\/(?:command|query)\/[^/]+\/[^/]+\.controller\.ts$/.test(path)) return;
490
+ const ofCaits = imported.some(({ name, from }) => name === "Request" && from === "@ingenioz-it/caits/messaging/Request");
491
+ const named = typeReferences.flatMap((name, index) => name === "globalThis.Request" || (name === "Request" && !ofCaits) ? [at(lines.typeReferences[index],
492
+ `This controller names the Request of the web, which reads its body once only, but a controller reads the Request of CAITS, which gives the content of the body at each call. ${ofCaits ? "Write `Request`, which the file imports from CAITS." : "Import it: `import type { Request } from \"@ingenioz-it/caits/messaging/Request\"`."}`)] : []);
493
+ const built = constructed.flatMap((name, index) => name !== "Request" ? [] : [at(lines.constructed[index],
494
+ "This controller builds a Request, but a controller reads the request that CAITS gives to its methods. Remove `new Request(…)`.")]);
495
+ return [...named, ...built];
496
+ }
497
+ },
454
498
 
455
499
  // Events of the frontend: what its views, its handlers and its context map publish
456
500
 
457
501
  {
458
502
  name: "frontend-event-shape",
459
- description: "The events of a frontend context are in event/, one file per event: event/{Event}.ts.",
503
+ description: "The events of a frontend context are in event/, one file per event, at its root.",
460
504
  on: "file",
461
505
  check: ({ path }) => {
462
506
  const [, context, rest] = /^(src\/frontend\/[^/]+)\/event\/(.+)$/.exec(path) ?? [];
463
507
  if (!rest || /^[A-Z]\w*\.ts$/.test(rest)) return;
464
- return `${rest} is not an event file: the events of a frontend context are in event/, one file per event, named after it. Move it to ${context}/event/${pascalOf(fileName(rest).replace(/\.[^.]+$/, ""))}.ts.`;
508
+ return `${rest} is not an event file: the events of a frontend context are in event/, one file per event, at its root. Move it to ${context}/event/${pascalOf(fileName(rest).replace(/\.[^.]+$/, ""))}.ts.`;
465
509
  }
466
510
  },
467
511
  {
468
512
  name: "event-file-exports-its-event",
469
- description: "A frontend event file exports one name only: the event, with the name of the file.",
513
+ description: "A frontend event file exports one name only: its event, a class, or the type that joins the events of the context.",
470
514
  on: "file",
471
515
  check: ({ path, exports }) => {
472
- const [, context, event] = /^src\/frontend\/([^/]+)\/event\/(\w+)\.ts$/.exec(path) ?? [];
473
- if (!event) return;
474
- const file = `${event}.ts`;
475
- if (exports.length === 0) return `${file} exports nothing, but an event file exports its event. Write \`export class ${event} { readonly type = "${typeOfEvent(context, event)}" as const; }\`.`;
516
+ const context = /^src\/frontend\/([^/]+)\/event\/\w+\.ts$/.exec(path)?.[1];
517
+ if (!context) return;
518
+ const file = fileName(path);
519
+ if (exports.length === 0) return `${file} exports nothing, but an event file exports its event. Write \`export class … { readonly type = "${context}.…" as const; }\`.`;
520
+ const names = exports.map(({ name }) => name ?? "default");
476
521
  if (exports.length > 1) {
477
- const names = exports.map(({ name }) => name ?? "default");
478
- const first = exports.find(({ name }) => name !== event) ?? exports[1];
479
- return at(first.line, `${file} exports ${exports.length} names (${names.join(", ")}), but an event file exports only its event, ${event}. Move ${listOf(names.filter(name => name !== event))} into ${event} as a static member, or to a file of its own.`);
522
+ return at(exports[1].line, `${file} exports ${exports.length} names (${names.join(", ")}), but an event file exports only its event. Move ${listOf(names.slice(1))} into ${names[0]} as a static member, or to a file of its own.`);
480
523
  }
481
524
  const [exported] = exports;
482
- if (exported.name !== event) return at(exported.line, `${file} exports ${exported.name ?? "a default"}, but an event file exports the event of its name, ${event}. Rename the export ${event}, or the file ${exported.name ?? "default"}.ts.`);
483
- if (!["class", "type"].includes(exported.kind)) return at(exported.line, `${file} exports ${event} as ${kindOf(exported.kind)}, but an event is a class, or a type that joins events. Write \`export class ${event}\`.`);
525
+ if (!["class", "type"].includes(exported.kind)) return at(exported.line, `${file} exports ${names[0]} as ${kindOf(exported.kind)}, but an event is a class, or a type that joins events. Write \`export class ${names[0]}\`.`);
484
526
  }
485
527
  },
486
528
  {
@@ -488,9 +530,10 @@ export const rules = [
488
530
  description: "A frontend event class has `readonly type = \"{context}.{event}\" as const`. The store reads this type: the context, then the name of the event without the context, in kebab-case.",
489
531
  on: "file",
490
532
  check: ({ path, classes }) => {
491
- const [, context, event] = /^src\/frontend\/([^/]+)\/event\/(\w+)\.ts$/.exec(path) ?? [];
492
- const declared = event && classes.find(({ exported, name }) => exported && name === event);
493
- if (!context || !declared) return;
533
+ const context = /^src\/frontend\/([^/]+)\/event\/\w+\.ts$/.exec(path)?.[1];
534
+ const declared = classes.find(({ exported, name }) => exported && name !== null);
535
+ if (!context || !declared?.name) return;
536
+ const event = declared.name;
494
537
  const type = typeOfEvent(context, event);
495
538
  const write = `\`readonly type = "${type}" as const\``;
496
539
  const property = declared.properties.find(({ name }) => name === "type");
@@ -586,7 +629,7 @@ export const rules = [
586
629
  if (event) return [at(lines.objectTypes[index], `${head} Build it with \`new ${event.name}(…)\`, from ${event.path}.`)];
587
630
  const [context, name] = type.split(".");
588
631
  const guess = `${pascalOf(context)}${pascalOf(name)}`;
589
- return [at(lines.objectTypes[index], `${head} Write the class ${guess} in src/frontend/${context}/event/${guess}.ts, with \`readonly type = "${type}" as const\`, and build the event with \`new ${guess}(…)\`.`)];
632
+ return [at(lines.objectTypes[index], `${head} Write the class ${guess}, with \`readonly type = "${type}" as const\`, in a file of its own in src/frontend/${context}/event/, and build the event with \`new ${guess}(…)\`.`)];
590
633
  });
591
634
  }
592
635
  },
@@ -634,49 +677,51 @@ export const rules = [
634
677
  name: "backend-infra-shape",
635
678
  description: "The infra of a backend context holds only adapters: infra/{port}/{implementation}-{port}.ts and their tests.",
636
679
  on: "file",
637
- check: ({ path, classes }) => {
638
- const [, context, rest] = /^(src\/backend\/[^/]+)\/infra\/(.+)$/.exec(path) ?? [];
680
+ check: (file, { root }) => {
681
+ const [, context, rest] = /^(src\/backend\/[^/]+)\/infra\/(.+)$/.exec(file.path) ?? [];
639
682
  if (!rest || /^([a-z0-9-]+)\/[a-z0-9-]+-\1(\.test)?\.ts$/.test(rest)) return;
640
683
  const parts = rest.split("/");
641
- const file = fileName(path);
684
+ const name = fileName(file.path);
642
685
  if (parts.length === 2 && /^[a-z0-9-]+$/.test(parts[0])) {
643
686
  const [folder] = parts;
644
- const test = file.endsWith(".test.ts") ? ".test" : "";
645
- const implementation = kebab(file.replace(/(\.test)?\.[^.]+$/, "")).replace(/[^a-z0-9-]+/g, "-").replace(new RegExp(`-?${folder}$`), "") || "{implementation}";
646
- return `${file} is in infra/${folder}/, but an adapter is named after its implementation and its port: {implementation}-${folder}.ts. Rename it ${implementation}-${folder}${test}.ts.`;
687
+ const test = name.endsWith(".test.ts") ? ".test" : "";
688
+ const implementation = kebab(name.replace(/(\.test)?\.[^.]+$/, "")).replace(/[^a-z0-9-]+/g, "-").replace(new RegExp(`-?${folder}$`), "") || "{implementation}";
689
+ return `${name} is in infra/${folder}/, but an adapter is named after its implementation and its port: {implementation}-${folder}.ts. Rename it ${implementation}-${folder}${test}.ts.`;
647
690
  }
648
- const port = classes.flatMap(({ implements: ports }) => ports)[0];
649
- return `infra/${rest} is in no folder of a port: the infra holds one folder per port, infra/{port}/. Move it into the folder of the port that it implements${port ? `, ${context}/infra/${kebab(port)}/` : ""}.`;
691
+ const port = implementedPortOf(file, root);
692
+ return `infra/${rest} is in no folder of a port: the infra holds one folder per port, infra/{port}/. Move it into the folder of the port that it implements${port ? `, ${context}/infra/${port.folder}/` : ""}.`;
650
693
  }
651
694
  },
652
695
  {
653
696
  name: "infra-folder-implements-port",
654
697
  description: "Each folder of infra/ is for a port that the domain declares: infra/{port}/ goes with domain/port/{port}/.",
655
698
  on: "file",
656
- check: ({ path, classes }, { paths }) => {
657
- const adapter = adapterOf(path);
658
- if (!adapter || paths.has(`${adapter.portModule}.ts`)) return;
659
- const context = contextFolderOf(path);
660
- const declared = (/** @type {string} */ port) => paths.has(`${context}/domain/port/${kebab(port)}/${port}.ts`);
661
- const implementer = classes.find(({ implements: ports }) => ports.some(declared));
662
- const port = implementer?.implements.find(declared);
663
- if (implementer && port) return `${implementer.name} implements ${port}, but it is in infra/${adapter.folder}/, and the domain declares no port ${adapter.port}. Move it to ${context}/infra/${kebab(port)}/.`;
664
- return `infra/${adapter.folder}/ is the folder of the port ${adapter.port}, but the domain declares no such port. Write the port in ${adapter.portModule}.ts, or move the adapter into the folder of the port that it implements.`;
699
+ check: (file, { paths, root }) => {
700
+ const adapter = adapterOf(file.path, paths);
701
+ if (!adapter || adapter.portModule) return;
702
+ const context = contextFolderOf(file.path);
703
+ const port = implementedPortOf(file, root);
704
+ if (port) return `${port.implementer} implements ${port.name}, but it is in infra/${adapter.folder}/, and the domain has no port in domain/port/${adapter.folder}/. Move it to ${context}/infra/${port.folder}/.`;
705
+ return `infra/${adapter.folder}/ is the folder of a port, but the domain has no port in domain/port/${adapter.folder}/. Write the port in ${context}/domain/port/${adapter.folder}/, or move the adapter into the folder of the port that it implements.`;
665
706
  }
666
707
  },
667
708
  {
668
709
  name: "adapter-implements-port",
669
- description: "An adapter in infra/{port}/ implements the port of its folder (`implements Port`).",
710
+ description: "An adapter in infra/{port}/ implements the port of its folder: the interface that it imports from domain/port/{port}/.",
670
711
  on: "file",
671
- check: ({ path, classes }, { paths }) => {
672
- const adapter = adapterOf(path);
673
- if (!adapter || adapter.test || !paths.has(`${adapter.portModule}.ts`) || classes.some(declared => declared.implements.includes(adapter.port))) return;
674
- const declared = classes.find(({ exported }) => exported) ?? classes[0];
675
- if (!declared) {
676
- return `${fileName(path)} holds no class, but an adapter of infra/${adapter.folder}/ is a class that implements ${adapter.port}. Write \`export class ${pascalOf(fileName(path).replace(/\.[^.]+$/, ""))} implements ${adapter.port}\`.`;
677
- }
712
+ check: (file, { paths, files, root }) => {
713
+ const adapter = adapterOf(file.path, paths);
714
+ if (!adapter?.portModule || adapter.test) return;
715
+ const imported = namesFrom(file, adapter.portModule, root);
716
+ if (file.classes.some(declared => declared.implements.some(name => imported.has(name)))) return;
717
+ const port = portNameOf(adapter.portModule, files);
718
+ const write = `with \`import type { ${port} } from "#context/${adapter.portModule.split("/").slice(3).join("/")}.js"\``;
719
+ const declared = file.classes.find(({ exported }) => exported) ?? file.classes[0];
720
+ if (!declared) return `${fileName(file.path)} holds no class, but an adapter of infra/${adapter.folder}/ is a class that implements ${port}. Write \`export class … implements ${port}\`, ${write}.`;
721
+ const name = declared.name ?? "Its class";
722
+ if (declared.implements.includes(port)) return at(declared.line, `${name} implements a ${port} that is not the port of infra/${adapter.folder}/, from ${adapter.portModule}.ts. Implement the port, ${write}.`);
678
723
  const others = declared.implements.length > 0 ? `; it implements ${listOf(declared.implements)}` : "";
679
- return at(declared.line, `${declared.name} does not implement ${adapter.port}, the port of infra/${adapter.folder}/${others}. Write \`export class ${declared.name} implements ${adapter.port}\`, with \`import type { ${adapter.port} } from "#context/domain/port/${adapter.folder}/${adapter.port}.js"\`.`);
724
+ return at(declared.line, `${name} does not implement ${port}, the port of infra/${adapter.folder}/${others}. Write \`export class ${declared.name ?? "…"} implements ${port}\`, ${write}.`);
680
725
  }
681
726
  },
682
727
  {
@@ -684,12 +729,15 @@ export const rules = [
684
729
  description: "The test of an adapter runs the contract of its port. Thus all the adapters of a port do the same things.",
685
730
  on: "file",
686
731
  check: (file, { files, paths, root }) => {
687
- const adapter = adapterOf(file.path);
688
- if (!adapter?.test || !paths.has(`${adapter.portModule}.ts`) || importedModules(file, root).includes(`${adapter.portModule}.contract`)) return;
689
- const tested = files.find(other => other.path === file.path.replace(/\.test\.ts$/, ".ts"));
690
- const name = tested?.classes.find(({ exported }) => exported)?.name ?? pascalOf(fileName(file.path).replace(/\.test\.ts$/, ""));
691
- const describe = files.find(other => other.path === `${adapter.portModule}.contract.ts`)?.exports.find(({ kind }) => kind === "function")?.name ?? `describe${adapter.port}Contract`;
692
- return `This test does not run the contract of ${adapter.port}, so nothing proves that ${name} does what every adapter of ${adapter.port} does. Import ${describe} from "#context/domain/port/${adapter.folder}/${adapter.port}.contract.js", and call it with a factory of ${name}.`;
732
+ const adapter = adapterOf(file.path, paths);
733
+ if (!adapter?.test || !adapter.portModule || importedModules(file, root).includes(`${adapter.portModule}.contract`)) return;
734
+ const port = portNameOf(adapter.portModule, files);
735
+ const testedPath = file.path.replace(/\.test\.ts$/, ".ts");
736
+ const tested = files.find(other => other.path === testedPath)?.classes.find(({ exported }) => exported)?.name;
737
+ const name = tested ?? fileName(testedPath);
738
+ const describe = files.find(other => other.path === `${adapter.portModule}.contract.ts`)?.exports.find(({ kind }) => kind === "function")?.name;
739
+ const contract = `#context/${adapter.portModule.split("/").slice(3).join("/")}.contract.js`;
740
+ return `This test does not run the contract of ${port}, so nothing proves that ${name} does what every adapter of ${port} does. Import ${describe ?? "the function of the contract"} from "${contract}", and call it with a factory of ${tested ?? "the adapter"}.`;
693
741
  }
694
742
  },
695
743
 
@@ -928,8 +976,9 @@ export const rules = [
928
976
  const bindings = [...paths].sort().flatMap(path => {
929
977
  const port = path.startsWith(`${context}/`) ? portOf(path) : null;
930
978
  if (!port || port.contract) return [];
931
- const adapter = adapters.filter(file => file.path.startsWith(`${context}/infra/${port.folder}/`)).flatMap(file => file.classes).find(declared => declared.implements.includes(port.name));
932
- return adapter ? [`bind(${port.name}).to(${adapter.name})`] : [];
979
+ const name = portNameOf(port.module, files);
980
+ const adapter = adapters.filter(file => file.path.startsWith(`${context}/infra/${port.folder}/`)).flatMap(file => file.classes).find(declared => declared.implements.includes(name));
981
+ return adapter ? [`bind(${name}).to(${adapter.name})`] : [];
933
982
  });
934
983
  return { file: context, message: `${context} is a context, but it has no di.ts, so the kernel does not find it. Write ${context}/di.ts with its bindings: \`export default [${bindings.join(", ")}]\`.` };
935
984
  });
@@ -1076,9 +1125,9 @@ export const rules = [
1076
1125
  name: "application-imports-no-infra",
1077
1126
  description: "The application (the handlers and the DTOs of the use cases, and the events of the frontend) does not import the infra: a handler depends on a port of the domain, and di.ts binds the adapter.",
1078
1127
  on: "import",
1079
- check: ({ from, to, target }) => {
1128
+ check: ({ from, to, target }, project) => {
1080
1129
  if (!withinContext(from, to) || from.layer !== "application" || to.layer !== "infra") return;
1081
- const port = portOfAdapter(target);
1130
+ const port = portOfAdapter(target, project);
1082
1131
  return `The application layer must not import the infra layer (${target}). Depend on its port${port ? `, ${port.name} (${port.path}),` : " in domain/port/"} instead, and bind the adapter in ${contextFolderOf(target)}/di.ts.`;
1083
1132
  }
1084
1133
  },
@@ -1086,9 +1135,9 @@ export const rules = [
1086
1135
  name: "presentation-imports-no-infra",
1087
1136
  description: "The presentation (a controller, or the presentation of the frontend) does not import the infra: it goes through a use case, whose handler depends on a port of the domain.",
1088
1137
  on: "import",
1089
- check: ({ file, from, to, target }) => {
1138
+ check: ({ file, from, to, target }, project) => {
1090
1139
  if (!withinContext(from, to) || from.layer !== "presentation" || to.layer !== "infra") return;
1091
- const port = portOfAdapter(target);
1140
+ const port = portOfAdapter(target, project);
1092
1141
  const named = port ? `${port.name} (${port.path})` : "its port";
1093
1142
  if (from.side === "frontend") {
1094
1143
  return `The presentation layer must not import the infra layer (${target}): a view publishes events, and the handlers of the use cases call the ports. Publish an event of ${from.context}, let the store send the DTO of a use case, and let its handler depend on ${named}.`;
@@ -3,12 +3,12 @@
3
3
  * Node API (`node:crypto`), or a folder of modules, ending with `/*` (`@ingenioz-it/caits/messaging/*`). A layer without a list imports
4
4
  * any package. The imports of the files of the project (`#context/…`, `./…`) are not packages: the other rules check them.
5
5
  * The domain holds the business rules: it imports only the primitives of the kernel. The application adds what a use case needs: its
6
- * input DTOs extend Input, and its handler publishes events. The presentation, the infra, di.ts and the context map have no list.
6
+ * handler publishes events, and reads the context of its request (its correlation id, and the signal of its client). The presentation, the infra, di.ts and the context map have no list.
7
7
  * The project starts from these lists in its quality/imports.mjs.
8
8
  */
9
9
  const domain = ["@ingenioz-it/caits/Result", "@ingenioz-it/caits/Token", "@ingenioz-it/caits/DomainEvent"];
10
10
 
11
11
  export const allowed = {
12
12
  domain,
13
- application: [...domain, "@ingenioz-it/caits/messaging/Input", "@ingenioz-it/caits/messaging/EventPublisher"]
13
+ application: [...domain, "@ingenioz-it/caits/messaging/EventPublisher", "@ingenioz-it/caits/di/RequestContext"]
14
14
  };
@@ -1,7 +1,14 @@
1
- /** The mutation tests. Stryker runs `caits mutation-suite` for each mutant: the unit tests, in a private copy of its sandbox. */
1
+ /**
2
+ * The mutation tests. Stryker runs the unit tests in Vitest, which stays started from one mutant to the next: for each mutant, the tests
3
+ * that run its code. A counter stops the code of a mutant that loops without end.
4
+ */
2
5
  export default {
3
- testRunner: "command",
4
- commandRunner: { command: "caits mutation-suite --bail=1 --no-isolate --maxWorkers=1" },
6
+ testRunner: "vitest",
7
+ // The tests find the code through the discovery of CAITS too, not only through their imports: the first run runs them all, and tells
8
+ // which test runs which code. The pool of Vitest by default: a fork of Node for the tests.
9
+ vitest: { configFile: "quality/vite.config.ts", related: false, pool: "forks" },
10
+ // A new project has no test yet. As soon as there is code, its mutants have no coverage, and fail the check.
11
+ allowEmpty: true,
5
12
  checkers: ["typescript"],
6
13
  tsconfigFile: "quality/caits/tsconfig.mutation.json",
7
14
  mutate: ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.contract.ts", "!src/**/test.di.ts", "!src/frontend/*/di.ts"],
@@ -13,8 +20,6 @@ export default {
13
20
  thresholds: { high: 100, low: 100, break: 100 },
14
21
  concurrency: "100%",
15
22
  timeoutMS: 15000,
16
- coverageAnalysis: "off",
17
- // The builds stay out of the sandbox: each suite builds its own (`caits mutation-suite`), and under `caits check`, the end-to-end tests
18
- // empty dist/ while Stryker copies the project.
23
+ // The builds stay out of the sandbox: under `caits check`, the end-to-end tests empty dist/ while Stryker copies the project.
19
24
  ignorePatterns: ["reports", "test-results", "playwright-report", "dist", ".marko-run"]
20
25
  };
@@ -1,7 +1,7 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import marko from "@marko/run/vite";
3
3
  import { defineConfig } from "vitest/config";
4
- import { kernel, wholeRouteTypes } from "@ingenioz-it/caits/vite";
4
+ import { adapter, kernel, wholeRouteTypes } from "@ingenioz-it/caits/vite";
5
5
 
6
6
  /**
7
7
  * The folder of the stylesheets of the app. Sass looks in it after the folder of a stylesheet: each stylesheet of the routes loads the
@@ -10,11 +10,21 @@ import { kernel, wholeRouteTypes } from "@ingenioz-it/caits/vite";
10
10
  */
11
11
  const STYLE_FOLDER = fileURLToPath(new URL("../../src/routes/_frontend/style", import.meta.url));
12
12
 
13
+ /**
14
+ * The build folder. `caits check` runs the checks at once: it gives each one a build folder of its own (CAITS_OUT_DIR), else they empty
15
+ * the folder of each other. So do the processes of the mutation tests, which Stryker numbers (STRYKER_MUTATOR_WORKER): the Marko plugin
16
+ * empties the entries of the routes in the build folder, then writes them again, as a test loads a route. Without either, the build goes
17
+ * in dist/, which `npm start` runs.
18
+ */
19
+ const worker = process.env.STRYKER_MUTATOR_WORKER;
20
+ const OUT_DIR = worker === undefined ? process.env.CAITS_OUT_DIR || "dist" : `node_modules/.caits/mutation/${worker}`;
21
+
13
22
  export default defineConfig({
14
- plugins: [wholeRouteTypes(), marko(), kernel()],
15
- // `caits check` runs the checks at once: it gives each one a build folder of its own (CAITS_OUT_DIR), else they empty the folder of
16
- // each other. Without it, the build goes in dist/, which `npm start` runs.
17
- build: { outDir: process.env.CAITS_OUT_DIR || "dist" },
23
+ // The adapter of CAITS: the server of `npm start` and that of `caits dev` boot the backend, start its timers, and stop it on SIGTERM.
24
+ plugins: [wholeRouteTypes(), marko({ adapter: adapter() }), kernel()],
25
+ // The build keeps the names of the classes: the errors of CAITS name them, and the build would else number a class that has the name
26
+ // of another, as a DTO of the backend and one of the frontend (`No handler takes AddMessageCommand$1`).
27
+ build: { outDir: OUT_DIR, rolldownOptions: { output: { keepNames: true } } },
18
28
  // The checks write their reports while the development server runs: a report is not a file of the app, and reloads no page. The
19
29
  // sandbox of the mutation tests (.stryker-tmp/) copies the project, its tsconfig.json too: the server would empty its cache at each
20
30
  // copy, and lose the dependencies that it optimized.