@ingenioz-it/caits 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/bin/caits.js +19 -12
  2. package/bin/init.js +16 -7
  3. package/package.json +1 -1
  4. package/quality/architecture/adapters/marko.mjs +7 -4
  5. package/quality/architecture/adapters/scss.mjs +29 -8
  6. package/quality/architecture/adapters/typescript.mjs +2 -2
  7. package/quality/architecture/facts.mjs +137 -30
  8. package/quality/architecture/project.mjs +114 -22
  9. package/quality/architecture.mjs +68 -22
  10. package/quality/check.mjs +20 -14
  11. package/quality/dev.mjs +3 -2
  12. package/quality/mutation-suite.mjs +56 -5
  13. package/quality/mutation-summary.mjs +29 -12
  14. package/quality/mutation.mjs +29 -0
  15. package/quality/terminal.mjs +30 -0
  16. package/quality/test-docs/test-docs.mjs +1 -2
  17. package/src/DomainEvent.ts +4 -1
  18. package/src/di/Container.ts +5 -1
  19. package/src/di/createBackend.ts +51 -8
  20. package/src/di/createFrontend.ts +7 -4
  21. package/src/di/discover.ts +35 -6
  22. package/src/di/discoverFrontends.ts +29 -6
  23. package/src/di/discovered.ts +3 -3
  24. package/src/di/discoveredFrontends.ts +3 -3
  25. package/src/di/frontend.ts +46 -4
  26. package/src/messaging/BackendEventBus.ts +4 -3
  27. package/src/messaging/ContextMap.ts +28 -4
  28. package/src/messaging/EventPublisher.ts +2 -2
  29. package/src/messaging/FrontendContextMap.ts +33 -0
  30. package/src/testing/backend/applications.ts +7 -6
  31. package/src/testing/backend/context-map.ts +4 -1
  32. package/src/testing/frontend/context-map.ts +6 -0
  33. package/src/testing/frontend/frontends.ts +16 -9
  34. package/src/virtual-modules.d.ts +3 -1
  35. package/src/vite.js +20 -13
  36. package/template/caits/architecture.mjs +1000 -177
  37. package/template/caits/eslint.js +18 -0
  38. package/template/caits/imports.mjs +14 -0
  39. package/template/caits/tsconfig.json +1 -1
  40. package/template/caits/vite.ts +4 -2
  41. package/template/project/AGENTS +9 -5
  42. package/template/project/quality/imports.mjs +10 -0
@@ -15,6 +15,9 @@ export default tseslint.config(
15
15
  js.configs.recommended,
16
16
  ...tseslint.configs.recommendedTypeChecked,
17
17
  {
18
+ // The code turns off no rule: a comment such as eslint-disable has no effect. An exception lives in quality/eslint.config.js, for the
19
+ // files that need it (the architecture rule exceptions-in-quality refuses the comment itself).
20
+ linterOptions: { noInlineConfig: true },
18
21
  languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: root } },
19
22
  rules: {
20
23
  "@typescript-eslint/no-floating-promises": "error",
@@ -25,6 +28,21 @@ export default tseslint.config(
25
28
  "no-empty-pattern": ["error", { allowObjectPatternsAsParameters: true }]
26
29
  }
27
30
  },
31
+ // A context map translates an event into others: what the new events hold comes from the event that it reads (and, in the backend,
32
+ // from its envelope), never from the clock, from chance, from the page or from a state that the map would keep.
33
+ {
34
+ files: ["src/*/*/context-map.ts"],
35
+ rules: {
36
+ "no-restricted-globals": ["error", ...["window", "document", "globalThis", "self", "localStorage", "sessionStorage", "navigator", "location", "fetch", "crypto", "performance", "process", "setTimeout", "setInterval", "queueMicrotask"]
37
+ .map(name => ({ name, message: "a context map reads only the event that it translates: the event of the context holds what the new events need" }))],
38
+ "no-restricted-properties": ["error", ...[["Date", "now"], ["Math", "random"]]
39
+ .map(([object, property]) => ({ object, property, message: "a context map reads only the event that it translates: the event of the context holds when and what" }))],
40
+ "no-restricted-syntax": ["error",
41
+ { selector: "NewExpression[callee.name='Date'][arguments.length=0]", message: "a context map reads only the event that it translates: the event of the context holds when" },
42
+ { selector: "Program > VariableDeclaration[kind!='const'], Program > ExportNamedDeclaration > VariableDeclaration[kind!='const']", message: "a context map keeps no state: each line gives the new events from the event alone" }
43
+ ]
44
+ }
45
+ },
28
46
  {
29
47
  files: ["test/**/*.ts", "src/**/*.test.ts", "src/**/*.contract.ts", "src/**/test.di.ts"],
30
48
  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" }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The packages that each layer may import, as CAITS gives them. An entry is a package (`uuid`), a module (`@ingenioz-it/caits/Token`), a
3
+ * Node API (`node:crypto`), or a folder of modules, ending with `/*` (`@ingenioz-it/caits/messaging/*`). A layer without a list imports
4
+ * any package. The imports of the files of the project (`#context/…`, `./…`) are not packages: the other rules check them.
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.
7
+ * The project starts from these lists in its quality/imports.mjs.
8
+ */
9
+ const domain = ["@ingenioz-it/caits/Result", "@ingenioz-it/caits/Token", "@ingenioz-it/caits/DomainEvent"];
10
+
11
+ export const allowed = {
12
+ domain,
13
+ application: [...domain, "@ingenioz-it/caits/messaging/Input", "@ingenioz-it/caits/messaging/EventPublisher"]
14
+ };
@@ -10,7 +10,7 @@
10
10
  "noEmit": true,
11
11
  "noImplicitOverride": true,
12
12
  "noUnusedLocals": true,
13
- "paths": { "#shared/*": ["../../src/backend/shared/*"] },
13
+ "paths": { "#shared/*": ["../../src/backend/shared/*"], "#frontend-shared/*": ["../../src/frontend/shared/*"] },
14
14
  "resolveJsonModule": true,
15
15
  "skipLibCheck": true,
16
16
  "strict": true,
@@ -15,8 +15,10 @@ export default defineConfig({
15
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
16
  // each other. Without it, the build goes in dist/, which `npm start` runs.
17
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/**"] } },
18
+ // 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
+ // sandbox of the mutation tests (.stryker-tmp/) copies the project, its tsconfig.json too: the server would empty its cache at each
20
+ // copy, and lose the dependencies that it optimized.
21
+ server: { watch: { ignored: ["**/reports/**", "**/.stryker-tmp/**"] } },
20
22
  css: { preprocessorOptions: { scss: { loadPaths: [STYLE_FOLDER] } } },
21
23
  test: {
22
24
  include: ["src/**/*.test.ts", "test/**/*.test.ts", "quality/**/*.test.mjs"],
@@ -11,18 +11,22 @@ A CAITS project (Clean Architecture In TypeScript): a Marko app with a backend a
11
11
 
12
12
  ## Where the code goes
13
13
 
14
- - The business lives in bounded contexts: `src/backend/<context>/` and `src/frontend/<context>/`, each with `domain/`, `application/`, `infra/`, `presentation/` and a `di.ts`. To add one, copy the shape of an existing one.
14
+ - The business lives in bounded contexts: `src/backend/<context>/` and `src/frontend/<context>/`, each with `domain/`, `infra/`, its use cases in `command/` and `query/`, and a `di.ts`. A frontend context has `event/` and `presentation/` too. To add one, copy the shape of an existing one.
15
15
  - Domain: entities, value objects, errors, domain events, and ports (`domain/port/<port>/`), each with its contract (`<Port>.contract.ts`).
16
- - Application: one folder per use case, `command/<use-case>/` or `query/<use-case>/`, with its DTOs, its handler and its test.
16
+ - Use cases: one folder per use case, `command/<use-case>/` or `query/<use-case>/`, which holds all of it: its DTOs, its handler and its test, `<use-case>.application.test.ts`, and in the backend its controllers, `<use-case>.<name>.controller.ts` (`<use-case>.http.controller.ts` for HTTP), each with its test, `<use-case>.<name>.presentation.test.ts`. An endpoint lives in one folder, and the name of a test says the layer that it tests. A use case imports nothing from another use case: what they share lives in the domain.
17
17
  - Infra: the adapters of the ports, in `infra/<port>/`. The test of each adapter runs the contract of its port.
18
- - Presentation: the controllers in the backend; the views and the store in the frontend.
19
- - Dependencies point to the domain. A context imports its own files with `#context/`, never with `../`.
20
- - `src/routes/` holds the pages and the HTTP handlers: a page places views, a handler gives the request to a controller.
18
+ - Frontend: its events, one file each in `event/`, and its presentation, which serves several use cases: the views (`presentation/view/<view>/`), which the routes place; the components, `presentation/component/<component>/`, the parts that the views place; and the store (`presentation/store/`), if the context has use cases. A view and a component take the frontend of their context only, publish its events and read its store. A view imports no other view, and a component no view.
19
+ - The contexts talk through the context shared, `src/backend/shared/` and `src/frontend/shared/`, and their context map, `context-map.ts` beside their di.ts. A handler (in the frontend, also a view or a component) publishes an event of its context; the context map, `share(Event, event => [...])`, turns it into events of shared on the shared bus (the event of the context holds what they need). The context map of each context that hears it, `when(Event, event => [...])`, turns it into input DTOs of the context (backend) or into events of the context (frontend). Each line goes one way, reads only its event, and the lines make no cycle. A context names only itself and shared, never another context: only its context map imports the events of shared.
20
+ - A component of the frontend context shared is a Marko tag, `component/<name>/index.marko`: a context places it by its name (`<badge>`), and `src/frontend/marko.json` gives Marko their folder.
21
+ - Dependencies point to the domain. A context imports its own files with `#context/`, and the context shared of its side with `#shared/` (backend) or `#frontend-shared/` (frontend), never with `../`.
22
+ - `src/routes/` holds the pages and the HTTP handlers, and no logic and no test: a page places views, a handler gives the request to a controller. A document that the pages give in another format than HTML (a sitemap, a feed) has its route in a folder named after its file, `` `sitemap.xml`/+handler.ts ``, which gives the GET of a view. The code that a test reads lives in a context.
21
23
 
22
24
  ## Rules
23
25
 
24
26
  - Write the test first. The coverage and the mutation score stay at 100%.
27
+ - A test reads only its own context and shared: no test crosses contexts.
25
28
  - A check that fails names the file and the rule: fix the code, not the rule.
29
+ - No comment turns off a check (`eslint-disable`, `@ts-ignore`, `v8 ignore`, `Stryker disable`), and no test is skipped. An exception lives in `quality/`, as narrow as it can be.
26
30
  - Do not change `quality/` (the rules, the thresholds, the configuration of the tools) without asking.
27
31
 
28
32
  Documentation: https://caits.dev/docs
@@ -0,0 +1,10 @@
1
+ import { allowed } from "./caits/imports.mjs";
2
+
3
+ /**
4
+ * The packages that each layer of this project may import, which `caits architecture` checks. They start from those of CAITS
5
+ * (quality/caits/imports.mjs): the domain and the application import only the kernel's primitives, and the other layers any package.
6
+ * Add a package to a list on purpose, for example:
7
+ * export default { ...allowed, domain: [...allowed.domain, "uuid"] };
8
+ * The keys are domain, application, presentation, infra, di and contextMap. A layer without a list imports any package.
9
+ */
10
+ export default allowed;