@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.
- package/bin/caits.js +20 -7
- package/bin/init.js +16 -1
- package/package.json +8 -5
- package/quality/architecture/adapters/scss.mjs +2 -2
- package/quality/architecture/facts.mjs +39 -11
- package/quality/architecture/project.mjs +8 -6
- package/quality/dev.mjs +6 -3
- package/quality/mutation-summary.mjs +30 -20
- package/quality/mutation.mjs +170 -14
- package/src/adapter.js +94 -0
- package/src/di/Container.ts +27 -0
- package/src/di/RequestContext.ts +8 -2
- package/src/di/backend.ts +12 -0
- package/src/di/createBackend.ts +197 -25
- package/src/di/declareDependencies.js +25 -5
- package/src/di/discover.ts +19 -9
- package/src/di/discoverFrontends.ts +4 -8
- package/src/di/discovered.ts +2 -2
- package/src/di/discoveredFrontends.ts +2 -2
- package/src/messaging/BackendEventBus.ts +2 -3
- package/src/messaging/ContextEvents.ts +11 -0
- package/src/messaging/ContextMap.ts +3 -4
- package/src/messaging/Controller.ts +36 -75
- package/src/messaging/EventSubscriber.ts +1 -1
- package/src/messaging/InProcessMessageBus.ts +8 -9
- package/src/messaging/MessageBus.ts +4 -2
- package/src/messaging/Request.ts +116 -0
- package/src/messaging/RequestEvents.ts +8 -0
- package/src/messaging/StreamController.ts +261 -0
- package/src/messaging/TimerController.ts +25 -0
- package/src/messaging/sse.ts +23 -0
- package/src/server/entry.ts +37 -0
- package/src/server/kernel.ts +19 -0
- package/src/server/packages.d.ts +12 -0
- package/src/server/serve.js +121 -0
- package/src/state/createStore.ts +1 -2
- package/src/testing/backend/application.ts +1 -1
- package/src/testing/backend/applicationScenario.ts +6 -7
- package/src/testing/backend/applications.ts +4 -1
- package/src/testing/backend/context-map.ts +1 -1
- package/src/testing/backend/integration.ts +1 -1
- package/src/testing/backend/presentationApplication.ts +23 -3
- package/src/testing/backend/presentationScenario.ts +84 -7
- package/src/testing/frontend/applicationScenario.ts +2 -3
- package/src/testing/frontend/applications.ts +3 -3
- package/src/testing/frontend/frontends.ts +3 -4
- package/src/testing/shared/FakeMessageBus.ts +39 -5
- package/src/virtual-modules.d.ts +0 -2
- package/src/vite.js +2 -2
- package/src/vite.types.d.ts +7 -0
- package/template/caits/architecture.mjs +162 -113
- package/template/caits/imports.mjs +2 -2
- package/template/caits/stryker.mjs +11 -6
- package/template/caits/vite.ts +15 -5
- package/template/project/AGENTS +1 -1
- package/quality/mutation-suite.mjs +0 -82
- 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
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
175
|
-
"value-object": "the
|
|
176
|
-
error: "the
|
|
177
|
-
event: "the
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
227
|
-
const first = exports.find(({ name }) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
246
|
-
const exported = exports
|
|
247
|
-
return exported
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
const declared = classes.find(({
|
|
259
|
-
if (!declared) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
return at(declared.line, declared.
|
|
264
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
return
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
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
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
const
|
|
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
|
|
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
|
|
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,
|
|
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:
|
|
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
|
|
473
|
-
if (!
|
|
474
|
-
const file =
|
|
475
|
-
if (exports.length === 0) return `${file} exports nothing, but an event file exports its event. Write \`export class
|
|
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
|
-
|
|
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.
|
|
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
|
|
492
|
-
const declared =
|
|
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}
|
|
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: ({
|
|
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
|
|
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 =
|
|
645
|
-
const implementation = kebab(
|
|
646
|
-
return `${
|
|
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 =
|
|
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/${
|
|
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: (
|
|
657
|
-
const adapter = adapterOf(path);
|
|
658
|
-
if (!adapter ||
|
|
659
|
-
const context = contextFolderOf(path);
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
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: ({
|
|
672
|
-
const adapter = adapterOf(path);
|
|
673
|
-
if (!adapter || adapter.test
|
|
674
|
-
const
|
|
675
|
-
if (
|
|
676
|
-
|
|
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, `${
|
|
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 || !
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
const
|
|
692
|
-
|
|
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
|
|
932
|
-
|
|
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
|
-
*
|
|
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/
|
|
13
|
+
application: [...domain, "@ingenioz-it/caits/messaging/EventPublisher", "@ingenioz-it/caits/di/RequestContext"]
|
|
14
14
|
};
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
/**
|
|
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: "
|
|
4
|
-
|
|
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
|
-
|
|
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
|
};
|
package/template/caits/vite.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
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.
|