@ingenioz-it/caits 0.0.0-stage → 0.5.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/LICENSE +5 -0
- package/README.md +36 -2
- package/bin/caits.js +184 -0
- package/bin/init.js +359 -0
- package/package.json +114 -4
- package/quality/architecture/adapters/marko.mjs +23 -0
- package/quality/architecture/adapters/scss.mjs +52 -0
- package/quality/architecture/adapters/typescript.mjs +13 -0
- package/quality/architecture/facts.mjs +159 -0
- package/quality/architecture/project.mjs +148 -0
- package/quality/architecture.mjs +116 -0
- package/quality/check.mjs +104 -0
- package/quality/dev.mjs +110 -0
- package/quality/e2e.mjs +125 -0
- package/quality/mutation-suite.mjs +31 -0
- package/quality/mutation-summary.mjs +41 -0
- package/quality/route-types.mjs +40 -0
- package/quality/test-docs/page.mjs +301 -0
- package/quality/test-docs/playwright-reporter.mjs +55 -0
- package/quality/test-docs/test-docs.mjs +180 -0
- package/quality/test-docs/vitest-reporter.mjs +38 -0
- package/src/DomainEvent.ts +4 -0
- package/src/Result.ts +6 -0
- package/src/Token.ts +8 -0
- package/src/di/Container.ts +123 -0
- package/src/di/DataDirectory.ts +6 -0
- package/src/di/Fetcher.ts +6 -0
- package/src/di/RequestContext.ts +6 -0
- package/src/di/backend.ts +19 -0
- package/src/di/createBackend.ts +106 -0
- package/src/di/createFrontend.ts +53 -0
- package/src/di/declareDependencies.js +93 -0
- package/src/di/discover.ts +39 -0
- package/src/di/discoverFrontends.ts +36 -0
- package/src/di/discovered.ts +6 -0
- package/src/di/discoveredFrontends.ts +6 -0
- package/src/di/frontend.ts +33 -0
- package/src/http/failureOf.ts +10 -0
- package/src/messaging/BackendEventBus.ts +60 -0
- package/src/messaging/ContextMap.ts +22 -0
- package/src/messaging/Controller.ts +109 -0
- package/src/messaging/EventPublisher.ts +12 -0
- package/src/messaging/EventSubscriber.ts +4 -0
- package/src/messaging/FrontendEventBus.ts +38 -0
- package/src/messaging/InProcessMessageBus.ts +35 -0
- package/src/messaging/Input.ts +4 -0
- package/src/messaging/MessageBus.ts +12 -0
- package/src/messaging/ReportError.ts +9 -0
- package/src/state/Loadable.ts +13 -0
- package/src/state/createStore.ts +45 -0
- package/src/state/follow.ts +7 -0
- package/src/state/request.ts +21 -0
- package/src/testing/backend/application.ts +12 -0
- package/src/testing/backend/applicationScenario.ts +60 -0
- package/src/testing/backend/applications.ts +62 -0
- package/src/testing/backend/context-map.ts +12 -0
- package/src/testing/backend/infra.ts +6 -0
- package/src/testing/backend/integration.ts +8 -0
- package/src/testing/backend/presentation.ts +14 -0
- package/src/testing/backend/presentationApplication.ts +25 -0
- package/src/testing/backend/presentationScenario.ts +47 -0
- package/src/testing/frontend/application.ts +12 -0
- package/src/testing/frontend/applicationScenario.ts +36 -0
- package/src/testing/frontend/applications.ts +70 -0
- package/src/testing/frontend/frontends.ts +28 -0
- package/src/testing/frontend/infra.ts +2 -0
- package/src/testing/frontend/marko.ts +12 -0
- package/src/testing/frontend/presentation.ts +7 -0
- package/src/testing/shared/FakeMessageBus.ts +58 -0
- package/src/testing/shared/tempDirectories.ts +15 -0
- package/src/virtual-modules.d.ts +33 -0
- package/src/vite.js +162 -0
- package/src/vite.types.d.ts +14 -0
- package/template/caits/architecture.mjs +467 -0
- package/template/caits/eslint.js +32 -0
- package/template/caits/marko-modules.d.ts +4 -0
- package/template/caits/playwright.ts +35 -0
- package/template/caits/stryker.mjs +20 -0
- package/template/caits/tsconfig.json +21 -0
- package/template/caits/tsconfig.mutation.json +5 -0
- package/template/caits/vite.ts +34 -0
- package/template/empty/src/routes/_frontend/+page.marko +9 -0
- package/template/empty/test/e2e/home.spec.ts +6 -0
- package/template/project/AGENTS +28 -0
- package/template/project/gitignore +16 -0
- package/template/project/nvmrc +1 -0
- package/template/project/quality/architecture.mjs +9 -0
- package/template/project/quality/eslint.config.js +6 -0
- package/template/project/quality/playwright.config.ts +10 -0
- package/template/project/quality/stryker.config.mjs +6 -0
- package/template/project/quality/vite.config.ts +5 -0
- package/template/project/tsconfig.json +4 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { adapterOf, frontendsOf, importedModules, isCore, isMarko, isNode, isPackage, kebab, kernelPathOf, locate, portOf, typeOfEvent } from "@ingenioz-it/caits/quality/architecture";
|
|
2
|
+
|
|
3
|
+
/** @import { Rule } from "@ingenioz-it/caits/quality/architecture" */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The rules of the architecture, by layer. Each rule has a name and a description, reads one kind of thing (`on`), and checks it:
|
|
7
|
+
* the check gives nothing when the rule holds, else what is wrong, as a message or a list of messages.
|
|
8
|
+
* - on "import": each import of the production code, as { file, from, to, specifier, internal, target } (from and to: see locate).
|
|
9
|
+
* The rules on imports do not overlap: an import breaks two of them only when it has two problems.
|
|
10
|
+
* - on "file": each file, with the project ({ root, files, paths }); `production: true` leaves the test code aside.
|
|
11
|
+
* - on "project": the project once; the check gives a list of { file, message }.
|
|
12
|
+
* @type {Rule[]}
|
|
13
|
+
*/
|
|
14
|
+
export const rules = [
|
|
15
|
+
// Kernel: the package @ingenioz-it/caits builds the containers and the controllers
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
name: "container-built-by-kernel",
|
|
19
|
+
description: "Only the kernel makes a Container. A context does not build its dependencies: its di.ts gives the bindings, and the kernel builds them.",
|
|
20
|
+
on: "file",
|
|
21
|
+
check: ({ constructed }) => constructed.includes("Container")
|
|
22
|
+
&& "only the kernel builds a container: a context says in its di.ts what each token stands for"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "controllers-built-by-kernel",
|
|
26
|
+
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.",
|
|
27
|
+
on: "file",
|
|
28
|
+
check: ({ constructed }) => {
|
|
29
|
+
const controller = constructed.find(name => name !== null && /^[A-Z]\w*Controller$/.test(name));
|
|
30
|
+
return controller && `only the kernel builds a controller: ask the kernel for it, or send a request through the router (new ${controller}(…))`;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Domain: entities, value objects, errors, events and ports
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
name: "domain-shape",
|
|
38
|
+
description: "The domain of a context holds only entities, value objects, errors, domain events and ports. Each kind of file has its folder and its name pattern.",
|
|
39
|
+
on: "file",
|
|
40
|
+
check: ({ path }) => /^src\/(backend|frontend)\/[^/]+\/domain\//.test(path)
|
|
41
|
+
&& !/^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)
|
|
42
|
+
&& "domain holds only entity/{Entity}.ts and its test, value-object/{ValueObject}.ts and its test, error/{Name}Error.ts, event/{DomainEvent}.ts, port/{port}/{Port}.ts and port/{port}/{Port}.contract.ts"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "domain-file-exports-one-name",
|
|
46
|
+
description: "A domain file exports one name only. A port file exports its interface and its token with the same name.",
|
|
47
|
+
on: "file",
|
|
48
|
+
declarations: new Set(["class", "interface", "type", "const", "function", "enum"]),
|
|
49
|
+
/** @this {{ declarations: Set<string> }} */
|
|
50
|
+
check({ path, exports }) {
|
|
51
|
+
if (!/^src\/(backend|frontend)\/[^/]+\/domain\/.+\.ts$/.test(path) || path.endsWith(".test.ts")) return;
|
|
52
|
+
const names = exports.filter(({ kind }) => this.declarations.has(kind)).map(({ name }) => name);
|
|
53
|
+
return (names.length !== exports.length || new Set(names).size !== 1) && `a domain file exports one name: a port and its token share it (found ${exports.length} exports)`;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "entity-is-class",
|
|
58
|
+
description: "An entity or a value object is an exported class. The class holds the business rules of its values.",
|
|
59
|
+
on: "file",
|
|
60
|
+
check: ({ path, classes }) => /^src\/(backend|frontend)\/[^/]+\/domain\/(entity|value-object)\/[A-Z]\w*\.ts$/.test(path) && !classes.some(({ exported }) => exported)
|
|
61
|
+
&& "an entity or a value object is a class"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "error-extends-error",
|
|
65
|
+
description: "A domain error is an exported class …Error that extends Error.",
|
|
66
|
+
on: "file",
|
|
67
|
+
check: ({ path, classes }) => /^src\/(backend|frontend)\/[^/]+\/domain\/error\/[A-Z]\w*Error\.ts$/.test(path)
|
|
68
|
+
&& !classes.some(({ exported, name, extends: parent }) => exported && name !== null && /^\w+Error$/.test(name) && parent === "Error")
|
|
69
|
+
&& "an error is a class that extends Error"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "port-folder-named-after-port",
|
|
73
|
+
description: "The folder of a port has the name of the port in kebab-case: the port NoteRepository is in port/note-repository/. Its adapters use the same folder name in the infra.",
|
|
74
|
+
on: "file",
|
|
75
|
+
check: ({ path }) => {
|
|
76
|
+
const port = portOf(path);
|
|
77
|
+
return port && kebab(port.name) !== port.folder && `the folder of port ${port.name} is port/${kebab(port.name)}/`;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "port-has-contract",
|
|
82
|
+
description: "Each port has a contract test, {Port}.contract.ts, next to it. The contract says what every adapter of the port must do.",
|
|
83
|
+
on: "file",
|
|
84
|
+
check: ({ path }, { paths }) => {
|
|
85
|
+
const port = portOf(path);
|
|
86
|
+
return port && !port.contract && !paths.has(`${port.module}.contract.ts`) && `a port has a contract: domain/port/${port.folder}/${port.name}.contract.ts`;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "contract-next-to-port",
|
|
91
|
+
description: "A contract test is in domain/port/{port}/, next to the port that it describes.",
|
|
92
|
+
on: "file",
|
|
93
|
+
check: ({ path }) => path.endsWith(".contract.ts") && !portOf(path) && "a contract test describes a port: it lives in domain/port/, next to the port"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "contract-runs-in-adapter-test",
|
|
97
|
+
description: "The test of at least one adapter runs each contract. A contract that no test runs checks nothing.",
|
|
98
|
+
on: "file",
|
|
99
|
+
check: ({ path }, { files, root }) => {
|
|
100
|
+
const port = portOf(path);
|
|
101
|
+
return port?.contract && !files.some(file => adapterOf(file.path)?.test && importedModules(file, root).includes(`${port.module}.contract`))
|
|
102
|
+
&& "a contract is run by the test of at least one adapter (in the context's infra)";
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "core-without-node",
|
|
107
|
+
description: "The domain and the application do not import Node APIs (node:*). Node APIs are technology: they stay in the infra.",
|
|
108
|
+
on: "import",
|
|
109
|
+
check: ({ from, specifier }) => isCore(from) && isNode(specifier) && "domain and application stay free of Node APIs"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "domain-uses-kernel-primitives",
|
|
113
|
+
description: "The domain imports only the files at the root of the kernel (@ingenioz-it/caits/Result, /Token, /DomainEvent). It does not import the buses, the store or the DI of the kernel.",
|
|
114
|
+
on: "import",
|
|
115
|
+
check: ({ from, specifier }) => from.layer === "domain" && kernelPathOf(specifier)?.includes("/")
|
|
116
|
+
&& "the domain imports only the kernel's domain primitives, never buses or the store"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "entity-without-port",
|
|
120
|
+
description: "An entity or a value object does not import a port. It holds the business rules; the application calls the ports.",
|
|
121
|
+
on: "import",
|
|
122
|
+
check: ({ file, from, target }) => from.layer === "domain" && /\/domain\/(entity|value-object)\//.test(file.path) && /\/domain\/port\//.test(target)
|
|
123
|
+
&& "an entity or a value object holds business rules: it never talks to a port, the application orchestrates"
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Application: the use cases, and the events of the frontend
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
name: "backend-application-shape",
|
|
130
|
+
description: "The application of a backend context holds only use cases. Each use case has its folder, command/{usecase}/ or query/{usecase}/, with its handler, its DTOs and its test.",
|
|
131
|
+
on: "file",
|
|
132
|
+
check: ({ path }) => /^src\/backend\/[^/]+\/application\//.test(path) && !/^src\/backend\/[^/]+\/application\/(command|query)\/([a-z0-9-]+)\/\2\.(handler|dto|test)\.ts$/.test(path)
|
|
133
|
+
&& "application holds only {usecase}.handler.ts, {usecase}.dto.ts and {usecase}.test.ts in command/{usecase}/ or query/{usecase}/"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "handler-answers-output",
|
|
137
|
+
description: "A backend handler gives its result as an Output DTO: it makes a `new …Output(…)`.",
|
|
138
|
+
on: "file",
|
|
139
|
+
check: ({ path, constructed }) => /^src\/backend\/[^/]+\/application\/.+\.handler\.ts$/.test(path) && !constructed.some(name => name !== null && /^\w+Output$/.test(name))
|
|
140
|
+
&& "a handler answers with `new SomethingOutput(…)`"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "frontend-application-shape",
|
|
144
|
+
description: "The application of a frontend context holds its use cases, as in the backend, and its events, one file per event in event/.",
|
|
145
|
+
on: "file",
|
|
146
|
+
check: ({ path }) => /^src\/frontend\/[^/]+\/application\//.test(path)
|
|
147
|
+
&& !/^src\/frontend\/[^/]+\/application\/((command|query)\/([a-z0-9-]+)\/\3\.(handler|dto|test)|event\/[A-Z]\w*)\.ts$/.test(path)
|
|
148
|
+
&& "a frontend application holds only {usecase}.handler.ts, {usecase}.dto.ts and {usecase}.test.ts in command/{usecase}/ or query/{usecase}/ and event/{Event}.ts"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "event-file-exports-its-event",
|
|
152
|
+
description: "A frontend event file exports one name only: the event, with the name of the file.",
|
|
153
|
+
on: "file",
|
|
154
|
+
check: ({ path, exports }) => {
|
|
155
|
+
const event = /^src\/frontend\/[^/]+\/application\/event\/(\w+)\.ts$/.exec(path)?.[1];
|
|
156
|
+
if (!event) return;
|
|
157
|
+
if (exports.length !== 1) return `an event file has exactly one export (found ${exports.length})`;
|
|
158
|
+
return (exports[0].name !== event || !["class", "type"].includes(exports[0].kind)) && `event/${event}.ts exports ${event}`;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "event-type-names-context",
|
|
163
|
+
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.",
|
|
164
|
+
on: "file",
|
|
165
|
+
check: ({ path, classes }) => {
|
|
166
|
+
const [, context, event] = /^src\/frontend\/([^/]+)\/application\/event\/(\w+)\.ts$/.exec(path) ?? [];
|
|
167
|
+
const declared = event && classes.find(({ exported, name }) => exported && name === event);
|
|
168
|
+
if (!context || !declared) return;
|
|
169
|
+
const type = typeOfEvent(context, event);
|
|
170
|
+
const property = declared.properties.find(({ name }) => name === "type");
|
|
171
|
+
return (!property?.readonly || !property.asConst || property.value !== type) && `the type of ${event} is "${type}": its context, then its name without the context`;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// Presentation: the controllers of the backend, the views and the stores of the frontend
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
name: "backend-presentation-shape",
|
|
179
|
+
description: "The presentation of a backend context holds only controllers, each in its folder {name}/ with its test.",
|
|
180
|
+
on: "file",
|
|
181
|
+
check: ({ path }) => /^src\/backend\/[^/]+\/presentation\//.test(path) && !/^src\/backend\/[^/]+\/presentation\/([a-z0-9-]+)\/\1\.(controller|test)\.ts$/.test(path)
|
|
182
|
+
&& "presentation holds only {name}/{name}.controller.ts and {name}/{name}.test.ts"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "controller-extends-controller",
|
|
186
|
+
description: "A controller is an exported class …Controller that extends the Controller of the kernel.",
|
|
187
|
+
on: "file",
|
|
188
|
+
check: ({ path, classes }) => /^src\/backend\/[^/]+\/presentation\/.+\.controller\.ts$/.test(path)
|
|
189
|
+
&& !classes.some(({ exported, name, extends: parent }) => exported && name !== null && /^\w+Controller$/.test(name) && parent === "Controller")
|
|
190
|
+
&& "a controller is an exported class …Controller that extends the kernel's Controller"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "frontend-presentation-shape",
|
|
194
|
+
description: "The presentation of a frontend context holds only its views (one folder per view), its store and the type of its frontend. The pages are in the routes.",
|
|
195
|
+
on: "file",
|
|
196
|
+
check: ({ path }) => /^src\/frontend\/[^/]+\/presentation\//.test(path)
|
|
197
|
+
&& !/^src\/frontend\/[^/]+\/presentation\/(view\/[a-z0-9-]+\/[^/]+|store\/[a-z]\w*Store\.ts|[A-Z]\w*Frontend\.ts)$/.test(path)
|
|
198
|
+
&& "a frontend presentation holds only view/{view}/ with the files that only this View uses, store/{context}Store.ts, {Context}Frontend.ts: the pages live in the routes"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "zod-in-presentation",
|
|
202
|
+
description: "Only the presentation imports zod. zod checks the data that comes from HTTP; the other layers get typed values.",
|
|
203
|
+
on: "import",
|
|
204
|
+
check: ({ from, specifier }) => isPackage(specifier, "zod") && (isCore(from) || (from.kind === "layer" && from.layer !== "presentation"))
|
|
205
|
+
&& "zod stays at the transport boundary (presentation)"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "marko-in-frontend-presentation",
|
|
209
|
+
description: "Only the frontend presentation and the routes import Marko. The other layers stay free of the view technology.",
|
|
210
|
+
on: "import",
|
|
211
|
+
check: ({ from, specifier }) => isMarko(specifier) && from.kind === "layer" && !(from.side === "frontend" && from.layer === "presentation")
|
|
212
|
+
&& "Marko lives in frontend presentation and routes only"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "dtos-sent-by-store",
|
|
216
|
+
description: "In the frontend, only the store and the application import the input DTOs. A view publishes events; the store turns them into DTOs and sends them to the message bus.",
|
|
217
|
+
on: "import",
|
|
218
|
+
check: ({ file, target }) => /^src\/frontend\/[^/]+\/application\/.+\.dto\.(js|ts)$/.test(target) && !/^src\/frontend\/[^/]+\/(presentation\/store|application)\//.test(file.path)
|
|
219
|
+
&& "in the frontend, only the store sends input DTOs to the message bus: a view publishes events, and the store turns them into DTOs"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "events-built-with-new",
|
|
223
|
+
description: "In the frontend, the code makes an event with `new SomeEvent(…)`, not as an object literal `{ type: \"…\" }`. The class gives the correct type and the correct data.",
|
|
224
|
+
on: "file",
|
|
225
|
+
check: ({ path, objectTypes }) => {
|
|
226
|
+
const type = /^src\/(frontend|routes\/_frontend)\//.test(path) && objectTypes.find(value => /^[a-z]+\.[a-z-]+$/.test(value));
|
|
227
|
+
return type && `an event of the frontend is built with \`new SomeEvent(…)\`, not as an object literal ({ type: "${type}" })`;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "views-take-own-frontend",
|
|
232
|
+
description: "A view takes only the frontend of its own context, with frontendOf. The contexts of the frontend do not share their frontends.",
|
|
233
|
+
on: "file",
|
|
234
|
+
production: true,
|
|
235
|
+
check: file => {
|
|
236
|
+
const where = locate(file.path);
|
|
237
|
+
return where.kind === "layer" && where.side === "frontend"
|
|
238
|
+
&& frontendsOf(file).filter(context => context !== where.context).map(context => `a context takes only its own frontend (${where.context}), not the one of ${context}`);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
// Infra: the adapters of the ports
|
|
243
|
+
|
|
244
|
+
{
|
|
245
|
+
name: "backend-infra-shape",
|
|
246
|
+
description: "The infra of a backend context holds only adapters: infra/{port}/{implementation}-{port}.ts and their tests.",
|
|
247
|
+
on: "file",
|
|
248
|
+
check: ({ path }) => /^src\/backend\/[^/]+\/infra\//.test(path) && !/^src\/backend\/[^/]+\/infra\/([a-z0-9-]+)\/[a-z0-9-]+-\1(\.test)?\.ts$/.test(path)
|
|
249
|
+
&& "infra holds only {port}/{implementation}-{port}.ts and its test"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "infra-folder-implements-port",
|
|
253
|
+
description: "Each folder of infra/ is for a port that the domain declares: infra/{port}/ goes with domain/port/{port}/.",
|
|
254
|
+
on: "file",
|
|
255
|
+
check: ({ path }, { paths }) => {
|
|
256
|
+
const adapter = adapterOf(path);
|
|
257
|
+
return adapter && !paths.has(`${adapter.portModule}.ts`) && `infra/${adapter.folder}/ implements a port: the domain must declare port/${adapter.port}`;
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "adapter-implements-port",
|
|
262
|
+
description: "An adapter in infra/{port}/ implements the port of its folder (`implements Port`).",
|
|
263
|
+
on: "file",
|
|
264
|
+
check: ({ path, classes }, { paths }) => {
|
|
265
|
+
const adapter = adapterOf(path);
|
|
266
|
+
return adapter && !adapter.test && paths.has(`${adapter.portModule}.ts`) && !classes.some(declared => declared.implements.includes(adapter.port))
|
|
267
|
+
&& `an adapter of infra/${adapter.folder}/ implements the port ${adapter.port}`;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "adapter-test-runs-contract",
|
|
272
|
+
description: "The test of an adapter runs the contract of its port. Thus all the adapters of a port do the same things.",
|
|
273
|
+
on: "file",
|
|
274
|
+
check: (file, { paths, root }) => {
|
|
275
|
+
const adapter = adapterOf(file.path);
|
|
276
|
+
return adapter?.test && paths.has(`${adapter.portModule}.ts`) && !importedModules(file, root).includes(`${adapter.portModule}.contract`)
|
|
277
|
+
&& `the test of an adapter runs the contract of its port (${adapter.port}.contract)`;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
// di.ts: the bindings of a context
|
|
282
|
+
|
|
283
|
+
{
|
|
284
|
+
name: "di-holds-bindings",
|
|
285
|
+
description: "A di.ts or a test.di.ts has one export, a list of bindings: `export default [bind(Port).to(Adapter), …]`. It builds nothing: the kernel reads the list and builds the dependencies.",
|
|
286
|
+
on: "file",
|
|
287
|
+
methods: new Set(["to", "toValue", "toFactory", "with"]),
|
|
288
|
+
/** @this {{ methods: Set<string> }} */
|
|
289
|
+
check({ path, exports, defaultArray }) {
|
|
290
|
+
if (!/^src\/(backend|frontend)\/[^/]+\/(test\.)?di\.ts$/.test(path)) return;
|
|
291
|
+
if (exports.length !== 1 || !defaultArray) return "a di.ts has one export: `export default [bind(Port).to(Adapter), …]`";
|
|
292
|
+
const others = defaultArray.filter(({ chain: [first, ...methods] }) => first !== "bind" || methods.length === 0 || !methods.every(method => this.methods.has(method)));
|
|
293
|
+
return others.length > 0 && `a di.ts holds bindings only: bind(Port).to(Adapter) (${others.map(({ source }) => source).join("; ")})`;
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "di-read-by-kernel",
|
|
298
|
+
description: "No layer imports the di.ts of a context. Only the kernel reads it.",
|
|
299
|
+
on: "import",
|
|
300
|
+
check: ({ from, to }) => from.kind === "layer" && to.layer === "di" && "a context's di.ts is read by the kernel only"
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
// Contexts: their folders, and what their layers import
|
|
304
|
+
|
|
305
|
+
{
|
|
306
|
+
name: "sides-hold-contexts",
|
|
307
|
+
description: "src/backend holds only the contexts and the context map (context-map.ts and its tests). src/frontend holds only the contexts.",
|
|
308
|
+
on: "file",
|
|
309
|
+
check: ({ path }) => {
|
|
310
|
+
const [, side, inside] = /^src\/(backend|frontend)\/([^/]+)$/.exec(path) ?? [];
|
|
311
|
+
return side && (side === "frontend" || !/^context-map(\.test|\.integration\.test)?\.ts$/.test(inside))
|
|
312
|
+
&& `src/${side} holds only the contexts${side === "backend" ? " and the context map (context-map.ts and its tests)" : ""}`;
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "context-content",
|
|
317
|
+
description: "A context holds only its four layers (domain, application, infra, presentation), its di.ts and its test.di.ts (and its package.json, which gives the shortcut #context).",
|
|
318
|
+
on: "file",
|
|
319
|
+
check: ({ path }) => /^src\/(backend|frontend)\/[^/]+\//.test(path) && !/^src\/(backend|frontend)\/[^/]+\/((domain|application|infra|presentation)\/|(di|test\.di)\.ts$)/.test(path)
|
|
320
|
+
&& "a context holds only domain, application, infra and presentation, with its di.ts and test.di.ts"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "context-has-di",
|
|
324
|
+
description: "Each folder of src/backend and src/frontend is a context, and has a di.ts. The kernel finds the contexts by their di.ts.",
|
|
325
|
+
on: "project",
|
|
326
|
+
check: ({ paths }) => {
|
|
327
|
+
const contexts = new Set([...paths].map(path => /^src\/(backend|frontend)\/[^/]+(?=\/)/.exec(path)?.[0]).filter(context => context !== undefined));
|
|
328
|
+
return [...contexts].filter(context => !paths.has(`${context}/di.ts`))
|
|
329
|
+
.map(context => ({ file: context, message: `a folder of ${context.split("/").slice(0, 2).join("/")} is a context: it says in di.ts what each of its tokens stands for` }));
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "imports-by-shortcut",
|
|
334
|
+
description: "In src, an import does not go up a folder (../). A context imports its own files with #context/ and the context shared with #shared/; a route imports the contexts with #backend/ and #frontend/. Thus an import does not change when its file moves. A stylesheet loads only the files of its folder: Sass reads # as the start of a fragment, and a View reads the values of the theme as CSS variables.",
|
|
335
|
+
on: "file",
|
|
336
|
+
check: ({ path, imports }) => path.startsWith("src/")
|
|
337
|
+
&& imports.filter(specifier => specifier.split("/").includes(".."))
|
|
338
|
+
.map(specifier => path.endsWith(".scss")
|
|
339
|
+
? `a stylesheet does not go up a folder: it loads the files of its folder, and a View reads the values of the theme as CSS variables (${specifier})`
|
|
340
|
+
: `an import does not go up a folder: use #context/ in a context, #shared/ for shared, #backend/ or #frontend/ in a route (${specifier})`)
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "contexts-isolated",
|
|
344
|
+
description: "A context does not import another context of its side, except the backend context shared. Backend contexts talk through events and the context map; frontend contexts do not talk: a route places their views side by side.",
|
|
345
|
+
on: "import",
|
|
346
|
+
check: ({ from, to }) => from.kind === "layer" && to.kind === "layer" && to.side === from.side && to.layer !== "di" && to.context !== from.context && to.context !== "shared"
|
|
347
|
+
&& (from.side === "backend"
|
|
348
|
+
? "a context never imports another context: it publishes events, and the context map turns them into the inputs of the others"
|
|
349
|
+
: "a context never imports another context: in the frontend, a route places the views of several contexts side by side")
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "sides-apart",
|
|
353
|
+
description: "A frontend context and a backend context do not import each other. They are two projects: they talk only through the HTTP API.",
|
|
354
|
+
on: "import",
|
|
355
|
+
check: ({ from, to }) => from.kind === "layer" && to.kind === "layer" && to.side !== from.side
|
|
356
|
+
&& "frontend and backend never import each other: they are two projects, which talk only through the HTTP API"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: "layer-direction",
|
|
360
|
+
description: "In a context, and from a context to shared, each layer imports only the layers that it may use. The domain imports the domain; the application adds the domain; the presentation adds the application; the infra imports the domain (and the application in the frontend); the di.ts imports the domain and the infra.",
|
|
361
|
+
on: "import",
|
|
362
|
+
allowed: {
|
|
363
|
+
backend: { domain: ["domain"], application: ["application", "domain"], presentation: ["presentation", "application", "domain"], infra: ["infra", "domain"], di: ["domain", "infra"] },
|
|
364
|
+
frontend: { domain: ["domain"], application: ["application", "domain"], presentation: ["presentation", "application", "domain"], infra: ["infra", "application", "domain"], di: ["domain", "infra"] }
|
|
365
|
+
},
|
|
366
|
+
/** @this {{ allowed: Record<string, Record<string, string[]>> }} */
|
|
367
|
+
check({ from, to }) {
|
|
368
|
+
return from.kind === "layer" && to.kind === "layer" && to.side === from.side && to.layer !== "di" && (to.context === from.context || to.context === "shared")
|
|
369
|
+
&& !this.allowed[from.side][from.layer]?.includes(to.layer) && `${from.layer} must not depend on ${to.layer}`;
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "contexts-without-routes",
|
|
374
|
+
description: "A context does not import the routes or the context map. The routes and the context map use the contexts, not the opposite.",
|
|
375
|
+
on: "import",
|
|
376
|
+
check: ({ from, to }) => from.kind === "layer" && (to.kind === "routes" || to.kind === "map") && "contexts never import routes or the context map"
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
// Context map: src/backend/context-map.ts, which turns the events of a context into the inputs of the others
|
|
380
|
+
|
|
381
|
+
{
|
|
382
|
+
name: "context-map-reads-events-and-dtos",
|
|
383
|
+
description: "The context map imports from the contexts only their domain events and their input DTOs. It turns an event of a context into the input of another context.",
|
|
384
|
+
on: "import",
|
|
385
|
+
check: ({ from, to, target }) => from.kind === "map" && to.kind === "layer" && !/\/domain\/event\/[^/]+$|\.dto\.(js|ts)$/.test(target)
|
|
386
|
+
&& "the context map reads only the events (domain/event) and the input DTOs (*.dto.ts) of the contexts"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "context-map-without-routes",
|
|
390
|
+
description: "The context map does not import the routes.",
|
|
391
|
+
on: "import",
|
|
392
|
+
check: ({ from, to }) => from.kind === "map" && to.kind === "routes" && "the context map never imports routes"
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
// Routes: the pages and the handlers of Marko Run
|
|
396
|
+
|
|
397
|
+
{
|
|
398
|
+
name: "routes-folders",
|
|
399
|
+
description: "src/routes holds only _frontend (the pages) and _backend (the HTTP handlers).",
|
|
400
|
+
on: "file",
|
|
401
|
+
check: ({ path }) => path.startsWith("src/routes/") && !/^src\/routes\/_(frontend|backend)\//.test(path) && "routes holds only _frontend and _backend"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "frontend-routes-content",
|
|
405
|
+
description: "src/routes/_frontend holds the pages (+page.marko), the layouts (+layout.marko), in component/ the components that only these pages use, in style/ the stylesheets (SCSS) of the app, and next to a page or a layout its stylesheet.",
|
|
406
|
+
on: "file",
|
|
407
|
+
check: ({ path }, { paths }) => path.startsWith("src/routes/_frontend/") && !/\/\+(page|layout)\.marko$/.test(path) && !/\/component\/[^/]+$/.test(path)
|
|
408
|
+
&& !/^src\/routes\/_frontend\/style\/([^/]+\/)*[^/]+\.scss$/.test(path)
|
|
409
|
+
&& !(path.endsWith(".scss") && ["+page.marko", "+layout.marko"].some(name => paths.has(`${path.slice(0, path.lastIndexOf("/"))}/${name}`)))
|
|
410
|
+
&& "routes/_frontend holds +page.marko, +layout.marko, in component/ the components that only its pages use, in style/ the stylesheets of the app, and next to a page or a layout its stylesheet"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "backend-routes-content",
|
|
414
|
+
description: "src/routes/_backend holds only +handler.ts files. The code of a handler lives in a context: the route only gives the request to a controller.",
|
|
415
|
+
on: "file",
|
|
416
|
+
check: ({ path }) => path.startsWith("src/routes/_backend/") && !path.endsWith("/+handler.ts") && "routes/_backend holds only +handler.ts: the rest lives in a context"
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "routes-on-their-side",
|
|
420
|
+
description: "A frontend route imports only the frontend; a backend route imports only the backend.",
|
|
421
|
+
on: "import",
|
|
422
|
+
check: ({ from, to }) => from.kind === "routes" && to.kind === "layer" && to.side !== from.side
|
|
423
|
+
&& "frontend and backend never import each other: they are two projects, which talk only through the HTTP API"
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "routes-through-presentation",
|
|
427
|
+
description: "A route imports only the presentation of the contexts of its side: the views for a page, the controllers for a handler.",
|
|
428
|
+
on: "import",
|
|
429
|
+
check: ({ from, to }) => from.kind === "routes" && to.kind === "layer" && to.side === from.side && to.layer !== "presentation" && "routes reach contexts through presentation"
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "routes-without-context-map",
|
|
433
|
+
description: "A route does not import the context map. The kernel reads it.",
|
|
434
|
+
on: "import",
|
|
435
|
+
check: ({ from, to }) => from.kind === "routes" && to.kind === "map" && "routes never import the context map: the kernel reads it"
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: "routes-take-no-frontend",
|
|
439
|
+
description: "A route does not call frontendOf. It only places the views; each view takes the frontend of its context.",
|
|
440
|
+
on: "file",
|
|
441
|
+
production: true,
|
|
442
|
+
check: file => locate(file.path).kind === "routes"
|
|
443
|
+
&& frontendsOf(file).map(context => `a route takes no frontend: the views of a context take theirs with frontendOf (${context})`)
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
// Test code: what the production code never imports
|
|
447
|
+
|
|
448
|
+
{
|
|
449
|
+
name: "no-test-environment",
|
|
450
|
+
description: "The production code does not import a test environment (test.di.ts). A test environment replaces dependencies in the tests only.",
|
|
451
|
+
on: "import",
|
|
452
|
+
check: ({ to }) => to.kind === "test-environment" && "production code never imports a test environment (test.di)"
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "no-test-support",
|
|
456
|
+
description: "The production code does not import the test support of the kernel (@ingenioz-it/caits/testing).",
|
|
457
|
+
on: "import",
|
|
458
|
+
check: ({ specifier }) => kernelPathOf(specifier)?.startsWith("testing/") && "production code never imports test support (@ingenioz-it/caits/testing)"
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: "nothing-outside-src",
|
|
462
|
+
description: "The production code in src imports no file outside src, test/ and quality/ included. It imports the kernel by its name, as a package, never by a path.",
|
|
463
|
+
on: "import",
|
|
464
|
+
check: ({ from, specifier, target }) => specifier.startsWith(".") && from.kind !== "other" && !target.startsWith("src/")
|
|
465
|
+
&& "production code (src) never imports a file outside src, tests included; it imports the kernel by its name (@ingenioz-it/caits/…)"
|
|
466
|
+
}
|
|
467
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import tseslint from "typescript-eslint";
|
|
4
|
+
import { writeRouteTypes } from "@ingenioz-it/caits/quality/route-types";
|
|
5
|
+
|
|
6
|
+
/** This file is in quality/caits/: the root of the project is two folders up. */
|
|
7
|
+
const root = join(import.meta.dirname, "../..");
|
|
8
|
+
|
|
9
|
+
// The lint checks the types of the handlers of the routes, which use the global `Run` of @marko/run: its types are in
|
|
10
|
+
// .marko-run/routes.d.ts, which git ignores. They are written again first, so that they exist, also in a new clone, and follow the routes.
|
|
11
|
+
await writeRouteTypes(root);
|
|
12
|
+
|
|
13
|
+
export default tseslint.config(
|
|
14
|
+
{ ignores: ["dist/**", "node_modules/**", ".marko-run/**", ".stryker-tmp/**", "coverage/**", "reports/**", "test-results/**", "quality/**/*.{js,mjs}", "**/*.marko"] },
|
|
15
|
+
js.configs.recommended,
|
|
16
|
+
...tseslint.configs.recommendedTypeChecked,
|
|
17
|
+
{
|
|
18
|
+
languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: root } },
|
|
19
|
+
rules: {
|
|
20
|
+
"@typescript-eslint/no-floating-promises": "error",
|
|
21
|
+
"@typescript-eslint/no-misused-promises": "error",
|
|
22
|
+
"@typescript-eslint/consistent-type-imports": "error",
|
|
23
|
+
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
|
24
|
+
"@typescript-eslint/no-explicit-any": "error",
|
|
25
|
+
"no-empty-pattern": ["error", { allowObjectPatternsAsParameters: true }]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
files: ["test/**/*.ts", "src/**/*.test.ts", "src/**/*.contract.ts", "src/**/test.di.ts"],
|
|
30
|
+
rules: { "@typescript-eslint/unbound-method": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/require-await": "off", "@typescript-eslint/no-empty-function": "off" }
|
|
31
|
+
}
|
|
32
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { defineConfig } from "@playwright/test";
|
|
6
|
+
|
|
7
|
+
/** This file is in quality/caits/: the root of the project is two folders up. */
|
|
8
|
+
const root = join(import.meta.dirname, "../..");
|
|
9
|
+
const port = 4310;
|
|
10
|
+
const fileOf = (specifier: string) => fileURLToPath(import.meta.resolve(specifier));
|
|
11
|
+
/** The folder of the data of the server: one for each project, also for two projects with the same name. */
|
|
12
|
+
const dataDirectory = join(tmpdir(), `${basename(root)}-${createHash("sha256").update(root).digest("hex").slice(0, 8)}-e2e-data`);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The end-to-end tests run on the production build. `caits e2e` starts the server of webServer itself, then Playwright uses it.
|
|
16
|
+
* The server keeps its data in DATA_DIR, which `caits e2e` empties before it starts the server. It builds in dist/, also under
|
|
17
|
+
* `caits check`, which gives each check a build folder of its own (CAITS_OUT_DIR): `npm run start` runs dist/index.mjs.
|
|
18
|
+
* The port must be free: `caits e2e` refuses to run the tests on a server that runs there already.
|
|
19
|
+
*/
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
testDir: join(root, "test/e2e"),
|
|
22
|
+
outputDir: join(root, "test-results"),
|
|
23
|
+
workers: 1,
|
|
24
|
+
fullyParallel: false,
|
|
25
|
+
reporter: [["list"], [fileOf("@ingenioz-it/caits/quality/test-docs/playwright-reporter")]],
|
|
26
|
+
use: { baseURL: `http://127.0.0.1:${port}` },
|
|
27
|
+
webServer: {
|
|
28
|
+
command: "npm run build && npm run start",
|
|
29
|
+
cwd: root,
|
|
30
|
+
url: `http://127.0.0.1:${port}/`,
|
|
31
|
+
timeout: 120_000,
|
|
32
|
+
reuseExistingServer: !!process.env.E2E_REUSE_SERVER,
|
|
33
|
+
env: { PORT: String(port), DATA_DIR: dataDirectory, CAITS_OUT_DIR: "" }
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The mutation tests. Stryker runs `caits mutation-suite` for each mutant: the unit tests, in a private copy of its sandbox. */
|
|
2
|
+
export default {
|
|
3
|
+
testRunner: "command",
|
|
4
|
+
commandRunner: { command: "caits mutation-suite --bail=1 --no-isolate --maxWorkers=1" },
|
|
5
|
+
checkers: ["typescript"],
|
|
6
|
+
tsconfigFile: "quality/caits/tsconfig.mutation.json",
|
|
7
|
+
mutate: ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.contract.ts", "!src/**/test.di.ts", "!src/frontend/*/di.ts"],
|
|
8
|
+
reporters: ["progress", "json", "html"],
|
|
9
|
+
htmlReporter: { fileName: "reports/mutation/index.html" },
|
|
10
|
+
jsonReporter: { fileName: "reports/mutation/mutation.json" },
|
|
11
|
+
tempDirName: ".stryker-tmp",
|
|
12
|
+
cleanTempDir: true,
|
|
13
|
+
thresholds: { high: 100, low: 100, break: 100 },
|
|
14
|
+
concurrency: "100%",
|
|
15
|
+
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.
|
|
19
|
+
ignorePatterns: ["reports", "test-results", "playwright-report", "dist", ".marko-run"]
|
|
20
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
// The paths start from this folder, quality/caits/: the root of the project is two folders up.
|
|
3
|
+
"include": ["../../src/**/*", "../../test/**/*", "../../.marko-run/routes.d.ts", "../../quality/**/*.ts"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"allowImportingTsExtensions": true,
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"lib": ["DOM", "ESNext"],
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"noImplicitOverride": true,
|
|
12
|
+
"noUnusedLocals": true,
|
|
13
|
+
"paths": { "#shared/*": ["../../src/backend/shared/*"] },
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"strict": true,
|
|
17
|
+
"target": "ESNext",
|
|
18
|
+
"types": ["node", "vite/client"],
|
|
19
|
+
"verbatimModuleSyntax": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import marko from "@marko/run/vite";
|
|
3
|
+
import { defineConfig } from "vitest/config";
|
|
4
|
+
import { kernel, wholeRouteTypes } from "@ingenioz-it/caits/vite";
|
|
5
|
+
|
|
6
|
+
/**
|
|
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
|
|
8
|
+
* partials of style/ by their name (`@use "values"`), from any folder. The architecture check reads the same load path: a context
|
|
9
|
+
* does not load the stylesheets of the routes.
|
|
10
|
+
*/
|
|
11
|
+
const STYLE_FOLDER = fileURLToPath(new URL("../../src/routes/_frontend/style", import.meta.url));
|
|
12
|
+
|
|
13
|
+
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" },
|
|
18
|
+
// The checks write their reports while the development server runs: a report is not a file of the app, and reloads no page.
|
|
19
|
+
server: { watch: { ignored: ["**/reports/**"] } },
|
|
20
|
+
css: { preprocessorOptions: { scss: { loadPaths: [STYLE_FOLDER] } } },
|
|
21
|
+
test: {
|
|
22
|
+
include: ["src/**/*.test.ts", "test/**/*.test.ts", "quality/**/*.test.mjs"],
|
|
23
|
+
// A new project has no test yet. The coverage asks for the tests of the code as soon as there is code.
|
|
24
|
+
passWithNoTests: true,
|
|
25
|
+
coverage: {
|
|
26
|
+
provider: "v8",
|
|
27
|
+
include: ["src/**/*.ts"],
|
|
28
|
+
exclude: ["src/**/*.test.ts", "src/**/*.contract.ts", "src/**/test.di.ts", "src/frontend/*/di.ts"],
|
|
29
|
+
reporter: ["text-summary", "text", "html"],
|
|
30
|
+
reportsDirectory: "reports/coverage",
|
|
31
|
+
thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|