@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.
Files changed (92) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +36 -2
  3. package/bin/caits.js +184 -0
  4. package/bin/init.js +359 -0
  5. package/package.json +114 -4
  6. package/quality/architecture/adapters/marko.mjs +23 -0
  7. package/quality/architecture/adapters/scss.mjs +52 -0
  8. package/quality/architecture/adapters/typescript.mjs +13 -0
  9. package/quality/architecture/facts.mjs +159 -0
  10. package/quality/architecture/project.mjs +148 -0
  11. package/quality/architecture.mjs +116 -0
  12. package/quality/check.mjs +104 -0
  13. package/quality/dev.mjs +110 -0
  14. package/quality/e2e.mjs +125 -0
  15. package/quality/mutation-suite.mjs +31 -0
  16. package/quality/mutation-summary.mjs +41 -0
  17. package/quality/route-types.mjs +40 -0
  18. package/quality/test-docs/page.mjs +301 -0
  19. package/quality/test-docs/playwright-reporter.mjs +55 -0
  20. package/quality/test-docs/test-docs.mjs +180 -0
  21. package/quality/test-docs/vitest-reporter.mjs +38 -0
  22. package/src/DomainEvent.ts +4 -0
  23. package/src/Result.ts +6 -0
  24. package/src/Token.ts +8 -0
  25. package/src/di/Container.ts +123 -0
  26. package/src/di/DataDirectory.ts +6 -0
  27. package/src/di/Fetcher.ts +6 -0
  28. package/src/di/RequestContext.ts +6 -0
  29. package/src/di/backend.ts +19 -0
  30. package/src/di/createBackend.ts +106 -0
  31. package/src/di/createFrontend.ts +53 -0
  32. package/src/di/declareDependencies.js +93 -0
  33. package/src/di/discover.ts +39 -0
  34. package/src/di/discoverFrontends.ts +36 -0
  35. package/src/di/discovered.ts +6 -0
  36. package/src/di/discoveredFrontends.ts +6 -0
  37. package/src/di/frontend.ts +33 -0
  38. package/src/http/failureOf.ts +10 -0
  39. package/src/messaging/BackendEventBus.ts +60 -0
  40. package/src/messaging/ContextMap.ts +22 -0
  41. package/src/messaging/Controller.ts +109 -0
  42. package/src/messaging/EventPublisher.ts +12 -0
  43. package/src/messaging/EventSubscriber.ts +4 -0
  44. package/src/messaging/FrontendEventBus.ts +38 -0
  45. package/src/messaging/InProcessMessageBus.ts +35 -0
  46. package/src/messaging/Input.ts +4 -0
  47. package/src/messaging/MessageBus.ts +12 -0
  48. package/src/messaging/ReportError.ts +9 -0
  49. package/src/state/Loadable.ts +13 -0
  50. package/src/state/createStore.ts +45 -0
  51. package/src/state/follow.ts +7 -0
  52. package/src/state/request.ts +21 -0
  53. package/src/testing/backend/application.ts +12 -0
  54. package/src/testing/backend/applicationScenario.ts +60 -0
  55. package/src/testing/backend/applications.ts +62 -0
  56. package/src/testing/backend/context-map.ts +12 -0
  57. package/src/testing/backend/infra.ts +6 -0
  58. package/src/testing/backend/integration.ts +8 -0
  59. package/src/testing/backend/presentation.ts +14 -0
  60. package/src/testing/backend/presentationApplication.ts +25 -0
  61. package/src/testing/backend/presentationScenario.ts +47 -0
  62. package/src/testing/frontend/application.ts +12 -0
  63. package/src/testing/frontend/applicationScenario.ts +36 -0
  64. package/src/testing/frontend/applications.ts +70 -0
  65. package/src/testing/frontend/frontends.ts +28 -0
  66. package/src/testing/frontend/infra.ts +2 -0
  67. package/src/testing/frontend/marko.ts +12 -0
  68. package/src/testing/frontend/presentation.ts +7 -0
  69. package/src/testing/shared/FakeMessageBus.ts +58 -0
  70. package/src/testing/shared/tempDirectories.ts +15 -0
  71. package/src/virtual-modules.d.ts +33 -0
  72. package/src/vite.js +162 -0
  73. package/src/vite.types.d.ts +14 -0
  74. package/template/caits/architecture.mjs +467 -0
  75. package/template/caits/eslint.js +32 -0
  76. package/template/caits/marko-modules.d.ts +4 -0
  77. package/template/caits/playwright.ts +35 -0
  78. package/template/caits/stryker.mjs +20 -0
  79. package/template/caits/tsconfig.json +21 -0
  80. package/template/caits/tsconfig.mutation.json +5 -0
  81. package/template/caits/vite.ts +34 -0
  82. package/template/empty/src/routes/_frontend/+page.marko +9 -0
  83. package/template/empty/test/e2e/home.spec.ts +6 -0
  84. package/template/project/AGENTS +28 -0
  85. package/template/project/gitignore +16 -0
  86. package/template/project/nvmrc +1 -0
  87. package/template/project/quality/architecture.mjs +9 -0
  88. package/template/project/quality/eslint.config.js +6 -0
  89. package/template/project/quality/playwright.config.ts +10 -0
  90. package/template/project/quality/stryker.config.mjs +6 -0
  91. package/template/project/quality/vite.config.ts +5 -0
  92. package/template/project/tsconfig.json +4 -0
@@ -0,0 +1,70 @@
1
+ /// <reference path="../../virtual-modules.d.ts" />
2
+ import { bindings as testDi } from "virtual:kernel/frontend-test";
3
+ import type { Binding } from "../../di/Container.js";
4
+ import { createFrontendWithBus } from "../../di/createFrontend.js";
5
+ import { discoveredFrontends } from "../../di/discoveredFrontends.js";
6
+ import type { Fetcher } from "../../di/Fetcher.js";
7
+ import type { Input } from "../../messaging/Input.js";
8
+ import type { Token } from "../../Token.js";
9
+
10
+ /** The test.di.ts of each context that has one: the bindings that replace, in tests, those that reach outside the browser. */
11
+ const testBindings: Record<string, readonly Binding[] | undefined> = Object.fromEntries(
12
+ Object.entries(testDi)
13
+ .map(([path, bindings]) => [/frontend\/([^/]+)\//.exec(path)![1], bindings])
14
+ );
15
+
16
+ const offline: Fetcher = input => Promise.reject(new Error(`A test frontend has no network: replace the dependency that asked for ${input}`));
17
+
18
+ type TestFrontend = ReturnType<typeof createFrontendWithBus>;
19
+
20
+ /**
21
+ * Every context, built from its di.ts with its test.di.ts over it, each one alone, and only when a test needs it. A test sends input
22
+ * DTOs, as the stores do, and reads the events that the handlers published.
23
+ */
24
+ export function createTestApplication() {
25
+ const published: object[] = [];
26
+ const built = new Map<string, TestFrontend>();
27
+
28
+ const frontend = (name: string): TestFrontend => {
29
+ let found = built.get(name);
30
+ if (!found) {
31
+ found = createFrontendWithBus(discoveredFrontends[name], offline, testBindings[name]);
32
+ found.events.subscribe(event => { published.push(event as object); });
33
+ built.set(name, found);
34
+ }
35
+ return found;
36
+ };
37
+
38
+ const ownerOf = (input: object): string => {
39
+ const name = Object.keys(discoveredFrontends).find(context => discoveredFrontends[context].useCases.some(({ input: type }) => type === input.constructor));
40
+ if (name === undefined) throw new Error(`No context handles ${input.constructor.name}`);
41
+ return name;
42
+ };
43
+
44
+ /** The contexts that bind this token. */
45
+ const holders = (token: Token<unknown>): string[] => {
46
+ const found = Object.keys(discoveredFrontends).filter(name => [...discoveredFrontends[name].bindings, ...testBindings[name] ?? []].some(binding => binding.key === token));
47
+ if (found.length === 0) throw new Error(`No context binds ${token.name}`);
48
+ return found;
49
+ };
50
+
51
+ return {
52
+ /** Sends an input DTO to the message bus of the context that handles it. */
53
+ send: <Output>(input: Input<Output>): Promise<Output> => frontend(ownerOf(input)).bus.send(input),
54
+ /** The dependency with this token, built now if no handler needed it before. */
55
+ get: <T>(token: Token<T>): T => {
56
+ const found = holders(token);
57
+ if (found.length > 1) throw new Error(`The contexts ${found.join(", ")} each build their own ${token.name}`);
58
+ return frontend(found[0]).get(token);
59
+ },
60
+ /** Gives the dependency that the app uses from now on: a test builds its own Fake, and keeps it to arrange and check it. */
61
+ use: <T, Given extends T>(token: Token<T>, value: Given): Given => {
62
+ for (const name of holders(token)) frontend(name).use(token, value);
63
+ return value;
64
+ },
65
+ /** Every event the handlers published, in the order they published them. */
66
+ published
67
+ };
68
+ }
69
+
70
+ export type TestApplication = ReturnType<typeof createTestApplication>;
@@ -0,0 +1,28 @@
1
+ import { FrontendEventBus } from "../../messaging/FrontendEventBus.js";
2
+ import type { Input } from "../../messaging/Input.js";
3
+ import { createStore, type StoreDefinition } from "../../state/createStore.js";
4
+ import type { Frontend } from "../../state/request.js";
5
+ import { discoveredFrontends } from "../../di/discoveredFrontends.js";
6
+ import { frontendsOn, type Frontends } from "../../di/frontend.js";
7
+
8
+ type StateOf<F> = F extends Frontend<infer S, unknown> ? S : never;
9
+ type EventOf<F> = F extends Frontend<unknown, infer E> ? E : never;
10
+
11
+ /**
12
+ * The presentation of a context, as its views get it: its EventPublisher and its store. The message bus of the store is a Fake: it
13
+ * records the DTOs it receives, and answers nothing. A test publishes the events of the handlers itself.
14
+ */
15
+ export function testPresentation<Name extends keyof Frontends>(name: Name): Frontend<StateOf<Frontends[Name]>, EventOf<Frontends[Name]>> & { sent: Input<unknown>[] } {
16
+ const events = new FrontendEventBus<EventOf<Frontends[Name]>>();
17
+ const sent: Input<unknown>[] = [];
18
+ const definition = discoveredFrontends[name].store as StoreDefinition<StateOf<Frontends[Name]>, EventOf<Frontends[Name]>>;
19
+ const store = createStore(definition, { events, bus: { send: <Output>(input: Input<Output>) => { sent.push(input); return Promise.resolve(undefined as Output); } } });
20
+ return { events, store, sent };
21
+ }
22
+
23
+ /** A $global as Marko gives it to the views: the address of the page, and frontends already built. */
24
+ export function globalWith(frontends: Partial<Frontends>, url = "http://localhost/"): object {
25
+ const $global = { url: new URL(url) };
26
+ for (const [name, frontend] of Object.entries(frontends)) frontendsOn($global).set(name, frontend);
27
+ return $global;
28
+ }
@@ -0,0 +1,2 @@
1
+ /** Everything an infra test of the frontend imports: the contract of a port, and the test of each implementation that runs it. */
2
+ export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
@@ -0,0 +1,12 @@
1
+ type Mountable = { mount(input: unknown, container: Element): { update(input: unknown): void; destroy(): void } };
2
+
3
+ export async function mountTemplate(template: unknown, input: unknown = {}) {
4
+ const container = document.createElement("div");
5
+ document.body.append(container);
6
+ const instance = (template as Mountable).mount(input, container);
7
+ await settle();
8
+ return { container, instance, async update(next: unknown) { instance.update(next); await settle(); }, async destroy() { instance.destroy(); await settle(); container.remove(); } };
9
+ }
10
+
11
+ export const settle = () => new Promise(resolve => setTimeout(resolve, 20));
12
+ export const press = async (element: Element | null | undefined) => { (element as HTMLElement).click(); await settle(); };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Everything a presentation test of the frontend imports: the test of a View. The file starts with `// @vitest-environment happy-dom`.
3
+ * `testPresentation` gives the real FrontendEventBus and Store of a context, with a Fake MessageBus that records the DTOs in `sent`.
4
+ */
5
+ export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
6
+ export { globalWith, testPresentation } from "./frontends.js";
7
+ export { mountTemplate, press, settle } from "./marko.js";
@@ -0,0 +1,58 @@
1
+ import type { MessageBus } from "../../messaging/MessageBus.js";
2
+
3
+ type Outcome = { reply: unknown } | { error: Error };
4
+ type Expectation = { message: object; outcome: Outcome | undefined; received: boolean };
5
+
6
+ function same(left: unknown, right: unknown): boolean {
7
+ if (Object.is(left, right)) return true;
8
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
9
+ if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right)) return false;
10
+ if (left instanceof Date) return left.getTime() === (right as Date).getTime();
11
+ const keys = Object.keys(left);
12
+ return keys.length === Object.keys(right).length && keys.every(key => Object.hasOwn(right, key) && same((left as Record<string, unknown>)[key], (right as Record<string, unknown>)[key]));
13
+ }
14
+
15
+ /** A message bus that only records: a test says which messages it expects, and what the application answers to each one. */
16
+ export class FakeMessageBus implements MessageBus {
17
+ private expectations: Expectation[] = [];
18
+ private unexpected: object[] = [];
19
+
20
+ expect(message: object): void {
21
+ this.expectations.push({ message, outcome: undefined, received: false });
22
+ }
23
+
24
+ replyToTheLast(output: unknown): void {
25
+ this.last("replies").outcome = { reply: output };
26
+ }
27
+
28
+ failTheLast(error: Error): void {
29
+ this.last("fails").outcome = { error };
30
+ }
31
+
32
+ send(message: object): Promise<never> {
33
+ const expected = this.expectations.find(candidate => !candidate.received && same(candidate.message, message));
34
+ if (!expected) {
35
+ this.unexpected.push(message);
36
+ const pending = this.expectations.filter(candidate => !candidate.received).map(candidate => JSON.stringify(candidate.message)).join(", ") || "none";
37
+ return Promise.reject(new Error(`The application sent a message nobody expected: ${message.constructor.name} ${JSON.stringify(message)} (still expected: ${pending})`));
38
+ }
39
+ expected.received = true;
40
+ if (!expected.outcome) return Promise.reject(new Error(`Nothing is prepared for the application to be answered: ${message.constructor.name}`));
41
+ return "error" in expected.outcome ? Promise.reject(expected.outcome.error) : Promise.resolve(expected.outcome.reply as never);
42
+ }
43
+
44
+ verify(): void {
45
+ const missing = this.expectations.filter(expectation => !expectation.received).map(expectation => expectation.message);
46
+ const problems = [
47
+ ...(this.unexpected.length ? [`messages the application sent although nobody expected them: ${JSON.stringify(this.unexpected)}`] : []),
48
+ ...(missing.length ? [`messages the application was expected to send: ${JSON.stringify(missing)}`] : [])
49
+ ];
50
+ if (problems.length) throw new Error(problems.join("\n"));
51
+ }
52
+
53
+ private last(step: string): Expectation {
54
+ const last = this.expectations.at(-1);
55
+ if (!last) throw new Error(`Say which message is expected before saying how the application ${step}.`);
56
+ return last;
57
+ }
58
+ }
@@ -0,0 +1,15 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll } from "vitest";
5
+
6
+ /** Gives a new empty folder at each call, and removes them all after the tests of the file. */
7
+ export function useTempDirectories() {
8
+ const directories: string[] = [];
9
+ afterAll(async () => { await Promise.all(directories.map(directory => rm(directory, { recursive: true, force: true }))); });
10
+ return async (): Promise<string> => {
11
+ const directory = await mkdtemp(join(tmpdir(), "contract-"));
12
+ directories.push(directory);
13
+ return directory;
14
+ };
15
+ }
@@ -0,0 +1,33 @@
1
+ /** The modules of the kernel's Vite plugin (vite.js): the files of the contexts, by their path from the root of the project. */
2
+ declare module "virtual:kernel/backend" {
3
+ import type { Binding } from "@ingenioz-it/caits/di/Container";
4
+ import type { Translation } from "@ingenioz-it/caits/messaging/ContextMap";
5
+ type Modules = Record<string, Record<string, unknown>>;
6
+ export const bindings: Record<string, readonly Binding[]>;
7
+ export const dtos: Modules;
8
+ export const handlers: Modules;
9
+ export const controllers: Modules;
10
+ export const map: Record<string, Translation[]>;
11
+ }
12
+
13
+ declare module "virtual:kernel/backend-test" {
14
+ import type { Binding } from "@ingenioz-it/caits/di/Container";
15
+ export const bindings: Record<string, readonly Binding[]>;
16
+ }
17
+
18
+ declare module "virtual:kernel/frontend" {
19
+ import type { Binding } from "@ingenioz-it/caits/di/Container";
20
+ import type { StoreDefinition } from "@ingenioz-it/caits/state/createStore";
21
+ type Modules = Record<string, Record<string, unknown>>;
22
+ export const bindings: Record<string, readonly Binding[]>;
23
+ export const dtos: Modules;
24
+ export const handlers: Modules;
25
+ export const stores: Record<string, StoreDefinition<unknown, unknown>>;
26
+ }
27
+
28
+ declare module "virtual:kernel/frontend-test" {
29
+ import type { Binding } from "@ingenioz-it/caits/di/Container";
30
+ export const bindings: Record<string, readonly Binding[]>;
31
+ }
32
+
33
+ declare module "virtual:marko-run/__marko-run__router.js";
package/src/vite.js ADDED
@@ -0,0 +1,162 @@
1
+ // JavaScript, not TypeScript: Node loads this file itself when Vite loads its configuration, and Node strips no types in node_modules,
2
+ // where a project installs the kernel. TypeScript checks the types of the comments; vite.types.d.ts gives them to the projects.
3
+ import { promises as files } from "node:fs";
4
+ import MagicString from "magic-string";
5
+ import { normalizePath, parseSync } from "vite";
6
+ import { declareDependencies } from "./di/declareDependencies.js";
7
+
8
+ /** @import { Plugin } from "vite" */
9
+ /** @import { KernelOptions } from "./vite.types.js" */
10
+
11
+ /** The name of this package: the app imports the tokens of the kernel (EventPublisher, DataDirectory, Fetcher…) with it. */
12
+ const KERNEL = "@ingenioz-it/caits";
13
+
14
+ /** The folder of the kernel: its classes and its tokens need the same code (a Controller gets its MessageBus, a token its name). */
15
+ const OWN_FOLDER = `${normalizePath(import.meta.dirname)}/`;
16
+
17
+ /**
18
+ * The plugins of the kernel, for Vite and Vitest:
19
+ * - the dependencies of the classes, for the containers (see declareDependencies);
20
+ * - the contexts, found by convention in {source}/backend and {source}/frontend (see discovered.ts and discoveredFrontends.ts).
21
+ * @param {KernelOptions} [options]
22
+ * @returns {Plugin[]}
23
+ */
24
+ export function kernel({ source = "src" } = {}) {
25
+ const folder = `/${source.replace(/^\/+|\/+$/g, "")}`;
26
+ return [dependencies(folder), contexts(folder)];
27
+ }
28
+
29
+ /**
30
+ * Writes, after the code of each module, the dependencies of its classes and the names of its tokens: in the modules whose classes the
31
+ * containers build, those of the project and those of the kernel. They are written before the minifier renames the code.
32
+ * @param {string} source
33
+ * @returns {Plugin}
34
+ */
35
+ function dependencies(source) {
36
+ /** @type {string[]} */
37
+ let folders = [];
38
+ return {
39
+ name: "kernel-dependencies",
40
+ enforce: "pre",
41
+ configResolved: config => { folders = [`${config.root}${source}/`, OWN_FOLDER]; },
42
+ transform(code, id) {
43
+ if (!id.endsWith(".ts") || !folders.some(folder => id.startsWith(folder))) return undefined;
44
+ const declarations = declareDependencies(code, id, parseSync, [KERNEL]);
45
+ if (declarations === undefined) return undefined;
46
+ // The written code maps to no line of the module: the stack traces and the coverage show only the code of the module.
47
+ const module = new MagicString(code).append(`\n${declarations}`);
48
+ return { code: module.toString(), map: module.generateMap({ hires: true, source: id }) };
49
+ }
50
+ };
51
+ }
52
+
53
+ /**
54
+ * The modules that find the contexts: each export of a module is a pattern of files, from the source folder, and whether it takes only
55
+ * the default export of each file. The test.di.ts files have modules of their own: the production code never reads them.
56
+ * @type {Record<string, [name: string, pattern: string, onlyDefault?: boolean][]>}
57
+ */
58
+ const CONTEXT_MODULES = {
59
+ "virtual:kernel/backend": [
60
+ ["bindings", "backend/*/di.ts", true],
61
+ ["dtos", "backend/*/application/*/*/*.dto.ts"],
62
+ ["handlers", "backend/*/application/*/*/*.handler.ts"],
63
+ ["controllers", "backend/*/presentation/*/*.controller.ts"],
64
+ ["map", "backend/context-map.ts", true]
65
+ ],
66
+ "virtual:kernel/backend-test": [["bindings", "backend/*/test.di.ts", true]],
67
+ "virtual:kernel/frontend": [
68
+ ["bindings", "frontend/*/di.ts", true],
69
+ ["dtos", "frontend/*/application/*/*/*.dto.ts"],
70
+ ["handlers", "frontend/*/application/*/*/*.handler.ts"],
71
+ ["stores", "frontend/*/presentation/store/*Store.ts", true]
72
+ ],
73
+ "virtual:kernel/frontend-test": [["bindings", "frontend/*/test.di.ts", true]]
74
+ };
75
+
76
+ /**
77
+ * The modules that find the contexts. Vite reads the patterns when it builds: they must be literal, and they start at the root of the
78
+ * project, because a virtual module has no folder.
79
+ * Vite and Vitest follow the files that a module loaded, but not the new files that its patterns find: when a file of a pattern comes
80
+ * or goes, the plugin makes the environments load the module again. It also tells the watcher that the files that import the module
81
+ * changed: Vitest, in watch mode, then runs again the tests that use them (a new handler is found without a restart of Vitest).
82
+ * @param {string} source
83
+ * @returns {Plugin}
84
+ */
85
+ function contexts(source) {
86
+ /** @type {(pattern: string, onlyDefault?: boolean) => string} */
87
+ const glob = (pattern, onlyDefault = false) =>
88
+ `import.meta.glob(${JSON.stringify(`${source}/${pattern}`)}, { eager: true${onlyDefault ? `, import: "default"` : ""} })`;
89
+ /** A pattern as a regular expression: `*` is a part of a name, without "/". */
90
+ const matcherOf = (/** @type {string} */ pattern) =>
91
+ new RegExp(`^${`${source}/${pattern}`.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", "[^/]*")}$`);
92
+ const modules = Object.entries(CONTEXT_MODULES).map(([id, exports]) => ({
93
+ id,
94
+ code: exports.map(([name, pattern, onlyDefault]) => `export const ${name} = ${glob(pattern, onlyDefault)};`).join("\n"),
95
+ matchers: exports.map(([, pattern]) => matcherOf(pattern))
96
+ }));
97
+ const moduleOf = (/** @type {string} */ id) => modules.find(module => module.id === id);
98
+ let root = "";
99
+ return {
100
+ name: "kernel-contexts",
101
+ config: () => ({
102
+ // Vite processes the package as the code of the project, also when it is installed: it holds TypeScript and imports these modules.
103
+ ssr: { noExternal: [KERNEL] },
104
+ optimizeDeps: { exclude: [KERNEL] },
105
+ // The shortcut #shared leads to the backend context shared from every file (the tsconfig.json of the project says it to TypeScript).
106
+ // Vite reads an alias before the "imports" of the package.json of a context (#context), which cannot lead out of the context.
107
+ resolve: { alias: [{ find: /^#shared\//, replacement: `${source}/backend/shared/` }] }
108
+ }),
109
+ configResolved: config => { root = config.root; },
110
+ configureServer: server => {
111
+ /** @param {string} file */
112
+ const reload = file => {
113
+ const path = normalizePath(file);
114
+ if (!path.startsWith(`${root}/`)) return;
115
+ const fromRoot = path.slice(root.length);
116
+ for (const { id } of modules.filter(({ matchers }) => matchers.some(matcher => matcher.test(fromRoot)))) {
117
+ /** @type {Set<string>} */
118
+ const importers = new Set();
119
+ for (const { moduleGraph } of Object.values(server.environments)) {
120
+ const loaded = moduleGraph.getModuleById(`\0${id}`);
121
+ if (!loaded) continue;
122
+ moduleGraph.invalidateModule(loaded);
123
+ for (const { file: importer } of loaded.importers) if (importer) importers.add(importer);
124
+ }
125
+ for (const importer of importers) server.watcher.emit("change", importer);
126
+ }
127
+ };
128
+ server.watcher.on("add", reload);
129
+ server.watcher.on("unlink", reload);
130
+ },
131
+ resolveId: id => moduleOf(id) ? `\0${id}` : undefined,
132
+ load: id => id.startsWith("\0") ? moduleOf(id.slice(1))?.code : undefined
133
+ };
134
+ }
135
+
136
+ /** The file of the types of the routes, which @marko/run writes in the folder .marko-run of the project. */
137
+ const ROUTE_TYPES = /[\\/]\.marko-run[\\/]routes\.d\.ts$/;
138
+ let routeTypesWrittenWhole = false;
139
+
140
+ /**
141
+ * Makes @marko/run write the types of the routes (.marko-run/routes.d.ts) whole. It writes the file in place each time it reads the
142
+ * routes (a build, a start of Vitest or of the development server): while it writes, the file is empty. `caits check` runs the checks
143
+ * at once, and the lint and the type check read this file: they saw it empty when another check started. Now the text goes in a file of
144
+ * its own, which then takes the name of the file. @marko/run gives no other way: the plugin replaces the writeFile of node:fs, for this
145
+ * file only, as soon as the configuration of Vite makes the plugin, before the plugins write anything.
146
+ * @returns {Plugin}
147
+ */
148
+ export function wholeRouteTypes() {
149
+ if (!routeTypesWrittenWhole) {
150
+ routeTypesWrittenWhole = true;
151
+ const { writeFile, rename } = files;
152
+ /** @type {typeof files.writeFile} */
153
+ const writeWhole = async (file, data, options) => {
154
+ if (typeof file !== "string" || !ROUTE_TYPES.test(file)) return writeFile(file, data, options);
155
+ const draft = `${file}.${process.pid}.tmp`;
156
+ await writeFile(draft, data, options);
157
+ await rename(draft, file);
158
+ };
159
+ files.writeFile = writeWhole;
160
+ }
161
+ return { name: "caits-whole-route-types" };
162
+ }
@@ -0,0 +1,14 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ /** The types of vite.js, for the projects: TypeScript reads no types from the JavaScript of a package. Keep them the same as vite.js. */
4
+
5
+ export type KernelOptions = {
6
+ /** The folder that holds backend/ and frontend/, from the root of the project. */
7
+ source?: string;
8
+ };
9
+
10
+ /** The plugins of the kernel, for Vite and Vitest: the dependencies of the classes, and the contexts of the project. */
11
+ export declare function kernel(options?: KernelOptions): Plugin[];
12
+
13
+ /** Makes @marko/run write the types of the routes whole: `caits check` runs the checks at once, and some of them read these types. */
14
+ export declare function wholeRouteTypes(): Plugin;