@pukujan/create-modular-monolith 2.0.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 (65) hide show
  1. package/README.md +43 -0
  2. package/bin/create-modular-monolith.js +132 -0
  3. package/package.json +39 -0
  4. package/template/README.md +73 -0
  5. package/template/backend/package-lock.json +882 -0
  6. package/template/backend/package.json +20 -0
  7. package/template/backend/scripts/check-module-boundaries.mjs +69 -0
  8. package/template/backend/scripts/check-module-layers.mjs +152 -0
  9. package/template/backend/src/core/module-loader.js +35 -0
  10. package/template/backend/src/core/server.js +24 -0
  11. package/template/backend/src/modules/.gitkeep +0 -0
  12. package/template/backend/src/modules/_reference/README.md +11 -0
  13. package/template/backend/src/modules/_reference/adapters/README.md +3 -0
  14. package/template/backend/src/modules/_reference/config/index.js +4 -0
  15. package/template/backend/src/modules/_reference/domain/README.md +3 -0
  16. package/template/backend/src/modules/_reference/evals/README.md +6 -0
  17. package/template/backend/src/modules/_reference/evals/datasets/example.cases.json +12 -0
  18. package/template/backend/src/modules/_reference/evals/runners/example.eval.mjs +25 -0
  19. package/template/backend/src/modules/_reference/events/index.js +4 -0
  20. package/template/backend/src/modules/_reference/index.js +9 -0
  21. package/template/backend/src/modules/_reference/prompts/manifest.json +14 -0
  22. package/template/backend/src/modules/_reference/prompts/templates/example.prompt.js +7 -0
  23. package/template/backend/src/modules/_reference/repositories/.gitkeep +0 -0
  24. package/template/backend/src/modules/_reference/routes/health.routes.js +10 -0
  25. package/template/backend/src/modules/_reference/routes/index.js +8 -0
  26. package/template/backend/src/modules/_reference/schemas/health.schema.js +8 -0
  27. package/template/backend/src/modules/_reference/services/health.service.js +7 -0
  28. package/template/backend/src/modules/_reference/tests/integration/health.routes.test.js +20 -0
  29. package/template/backend/src/modules/_reference/tests/unit/health.service.test.js +9 -0
  30. package/template/backend/src/modules/_reference/utils/index.js +3 -0
  31. package/template/backend/src/shared/ai/prompt-registry.js +42 -0
  32. package/template/backend/src/shared/events/index.js +8 -0
  33. package/template/backend/src/shared/http/errors.js +10 -0
  34. package/template/backend/src/shared/testing/create-test-app.js +13 -0
  35. package/template/docs/DEVLOG_V2.md +369 -0
  36. package/template/docs/PUBLISHING.md +39 -0
  37. package/template/docs/README.md +13 -0
  38. package/template/docs/STARTER_PACK.md +98 -0
  39. package/template/docs/architecture/ARCHITECTURE_GUARDRAILS.md +74 -0
  40. package/template/docs/architecture/MODULE_INTERNAL_CONTRACT.md +164 -0
  41. package/template/frontend/index.html +12 -0
  42. package/template/frontend/package-lock.json +1724 -0
  43. package/template/frontend/package.json +21 -0
  44. package/template/frontend/src/core/App.jsx +35 -0
  45. package/template/frontend/src/core/moduleRegistry.jsx +39 -0
  46. package/template/frontend/src/index.css +53 -0
  47. package/template/frontend/src/main.jsx +10 -0
  48. package/template/frontend/src/modules/.gitkeep +0 -0
  49. package/template/frontend/src/modules/_reference/README.md +3 -0
  50. package/template/frontend/src/modules/_reference/components/ModuleHealthCard.jsx +14 -0
  51. package/template/frontend/src/modules/_reference/hooks/use-module-health.js +27 -0
  52. package/template/frontend/src/modules/_reference/index.jsx +7 -0
  53. package/template/frontend/src/modules/_reference/pages/_referencePage.jsx +11 -0
  54. package/template/frontend/src/modules/_reference/prompts/README.md +3 -0
  55. package/template/frontend/src/modules/_reference/schemas/health.schema.js +3 -0
  56. package/template/frontend/src/modules/_reference/services/health-api.js +5 -0
  57. package/template/frontend/src/modules/_reference/tests/unit/health.schema.test.js +8 -0
  58. package/template/frontend/src/modules/_reference/utils/index.js +3 -0
  59. package/template/frontend/src/shared/api/client.js +10 -0
  60. package/template/frontend/vite.config.js +6 -0
  61. package/template/package.json +16 -0
  62. package/template/scripts/lib/module-scaffold.mjs +409 -0
  63. package/template/scripts/new-module.mjs +58 -0
  64. package/template/scripts/run-module-evals.mjs +43 -0
  65. package/template/scripts/sync-cli-template.mjs +44 -0
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "modular-monolith-starter-backend",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "node --watch src/core/server.js",
8
+ "start": "node src/core/server.js",
9
+ "lint:boundaries": "node scripts/check-module-boundaries.mjs",
10
+ "lint:layers": "node scripts/check-module-layers.mjs",
11
+ "lint:architecture": "npm run lint:boundaries && npm run lint:layers",
12
+ "test": "node --test 'src/modules/**/tests/**/*.test.js'",
13
+ "test:evals": "node ../scripts/run-module-evals.mjs"
14
+ },
15
+ "dependencies": {
16
+ "cors": "^2.8.5",
17
+ "dotenv": "^16.4.5",
18
+ "express": "^4.19.2"
19
+ }
20
+ }
@@ -0,0 +1,69 @@
1
+ import { readdirSync, readFileSync, existsSync } from "fs";
2
+ import { join, relative } from "path";
3
+
4
+ const apps = [
5
+ { name: "backend", root: new URL("../", import.meta.url).pathname, modulesSubpath: "src/modules" },
6
+ {
7
+ name: "frontend",
8
+ root: new URL("../../frontend/", import.meta.url).pathname,
9
+ modulesSubpath: "src/modules"
10
+ }
11
+ ];
12
+
13
+ const forbidden = [];
14
+
15
+ for (const app of apps) {
16
+ const modulesDir = join(app.root, app.modulesSubpath);
17
+ if (!existsSync(modulesDir)) continue;
18
+
19
+ const moduleNames = readdirSync(modulesDir, { withFileTypes: true })
20
+ .filter((d) => d.isDirectory())
21
+ .filter((d) => !d.name.startsWith("."))
22
+ .map((d) => d.name);
23
+
24
+ for (const moduleName of moduleNames) {
25
+ const moduleRoot = join(modulesDir, moduleName);
26
+ const files = walk(moduleRoot).filter((f) =>
27
+ [".js", ".mjs", ".jsx"].some((ext) => f.endsWith(ext))
28
+ );
29
+
30
+ for (const file of files) {
31
+ const source = readFileSync(file, "utf8");
32
+
33
+ for (const other of moduleNames) {
34
+ if (other === moduleName) continue;
35
+ const needle = `/modules/${other}/`;
36
+ if (source.includes(needle)) {
37
+ forbidden.push({
38
+ app: app.name,
39
+ file: relative(app.root, file),
40
+ moduleName,
41
+ other,
42
+ needle
43
+ });
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ if (forbidden.length) {
51
+ console.error("Module boundary violations found:\n");
52
+ for (const hit of forbidden) {
53
+ console.error(`- [${hit.app}] ${hit.file} references cross-module path (${hit.needle})`);
54
+ }
55
+ process.exit(1);
56
+ }
57
+
58
+ console.log("Module boundaries OK.");
59
+
60
+ function walk(dir) {
61
+ const entries = readdirSync(dir, { withFileTypes: true });
62
+ const files = [];
63
+ for (const entry of entries) {
64
+ const full = join(dir, entry.name);
65
+ if (entry.isDirectory()) files.push(...walk(full));
66
+ else files.push(full);
67
+ }
68
+ return files;
69
+ }
@@ -0,0 +1,152 @@
1
+ import { readdirSync, readFileSync, existsSync } from "fs";
2
+ import { join, relative } from "path";
3
+
4
+ const root = new URL("../", import.meta.url).pathname;
5
+ const modulesDir = join(root, "src/modules");
6
+
7
+ const LAYERS = [
8
+ "routes",
9
+ "services",
10
+ "repositories",
11
+ "adapters",
12
+ "domain",
13
+ "events",
14
+ "prompts",
15
+ "evals",
16
+ "schemas",
17
+ "utils",
18
+ "config"
19
+ ];
20
+
21
+ const FORBIDDEN_IMPORTS = {
22
+ domain: ["services", "routes", "repositories", "adapters", "events", "prompts", "evals"],
23
+ routes: ["repositories", "adapters", "domain", "events", "prompts", "evals"],
24
+ repositories: ["services", "routes", "events", "evals", "prompts"],
25
+ prompts: ["services", "routes", "repositories", "adapters", "events", "evals"],
26
+ utils: ["services", "routes", "repositories", "adapters", "domain", "events", "prompts", "evals"],
27
+ schemas: ["services", "routes", "repositories", "adapters", "domain", "events", "evals", "prompts"],
28
+ adapters: ["services", "routes", "events", "evals", "prompts"],
29
+ events: ["routes", "repositories", "adapters", "prompts", "evals"],
30
+ evals: ["routes", "events", "repositories", "adapters"],
31
+ config: ["routes", "services", "repositories", "adapters", "domain", "events", "prompts", "evals"]
32
+ };
33
+
34
+ if (!existsSync(modulesDir)) {
35
+ console.log("No modules directory found. Skipping.");
36
+ process.exit(0);
37
+ }
38
+
39
+ const moduleNames = readdirSync(modulesDir, { withFileTypes: true })
40
+ .filter((d) => d.isDirectory())
41
+ .filter((d) => !d.name.startsWith("_"))
42
+ .map((d) => d.name);
43
+
44
+ const violations = [];
45
+
46
+ for (const moduleName of moduleNames) {
47
+ const moduleRoot = join(modulesDir, moduleName);
48
+ const files = walk(moduleRoot).filter((f) => {
49
+ if (f.includes("/tests/") || f.includes("\\tests\\")) return false;
50
+ if (f.endsWith(".test.js") || f.endsWith(".test.mjs")) return false;
51
+ return f.endsWith(".js") || f.endsWith(".mjs");
52
+ });
53
+
54
+ for (const file of files) {
55
+ const fromLayer = layerForFile(file, moduleRoot);
56
+ if (!fromLayer || fromLayer === "index") continue;
57
+
58
+ const forbidden = FORBIDDEN_IMPORTS[fromLayer];
59
+ if (!forbidden) continue;
60
+
61
+ const source = readFileSync(file, "utf8");
62
+ const imports = extractRelativeImports(source);
63
+
64
+ for (const imp of imports) {
65
+ const toLayer = layerForImportPath(imp, file, moduleRoot);
66
+ if (!toLayer) continue;
67
+ if (forbidden.includes(toLayer)) {
68
+ violations.push({
69
+ file: relative(root, file),
70
+ fromLayer,
71
+ toLayer,
72
+ importPath: imp
73
+ });
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ if (violations.length) {
80
+ console.error("Module layer violations found:\n");
81
+ for (const hit of violations) {
82
+ console.error(
83
+ `- ${hit.file} (${hit.fromLayer}) must not import ${hit.toLayer} via "${hit.importPath}"`
84
+ );
85
+ }
86
+ console.error("\nSee docs/architecture/MODULE_INTERNAL_CONTRACT.md");
87
+ process.exit(1);
88
+ }
89
+
90
+ console.log("Module layers OK.");
91
+
92
+ function layerForFile(filePath, moduleRoot) {
93
+ const rel = relative(moduleRoot, filePath);
94
+ if (rel === "index.js") return "index";
95
+ const segment = rel.split(/[/\\]/)[0];
96
+ return LAYERS.includes(segment) ? segment : null;
97
+ }
98
+
99
+ function layerForImportPath(importPath, fromFile, moduleRoot) {
100
+ const fromDir = join(fromFile, "..");
101
+ let resolved = importPath.startsWith(".")
102
+ ? join(fromDir, importPath)
103
+ : null;
104
+
105
+ if (!resolved) {
106
+ for (const layer of LAYERS) {
107
+ if (
108
+ importPath.includes(`/${layer}/`) ||
109
+ importPath.startsWith(`${layer}/`) ||
110
+ importPath.includes(`../${layer}/`)
111
+ ) {
112
+ return layer;
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+
118
+ if (!resolved.endsWith(".js") && !resolved.endsWith(".mjs")) {
119
+ resolved += ".js";
120
+ }
121
+
122
+ const rel = relative(moduleRoot, resolved);
123
+ const segment = rel.split(/[/\\]/)[0];
124
+ return LAYERS.includes(segment) ? segment : null;
125
+ }
126
+
127
+ function extractRelativeImports(source) {
128
+ const imports = [];
129
+ const patterns = [
130
+ /from\s+["'](\.[^"']+)["']/g,
131
+ /import\s*\(\s*["'](\.[^"']+)["']\s*\)/g,
132
+ /require\s*\(\s*["'](\.[^"']+)["']\s*\)/g
133
+ ];
134
+ for (const pattern of patterns) {
135
+ let match;
136
+ while ((match = pattern.exec(source)) !== null) {
137
+ imports.push(match[1]);
138
+ }
139
+ }
140
+ return imports;
141
+ }
142
+
143
+ function walk(dir) {
144
+ const entries = readdirSync(dir, { withFileTypes: true });
145
+ const files = [];
146
+ for (const entry of entries) {
147
+ const full = join(dir, entry.name);
148
+ if (entry.isDirectory()) files.push(...walk(full));
149
+ else files.push(full);
150
+ }
151
+ return files;
152
+ }
@@ -0,0 +1,35 @@
1
+ import { readdirSync, existsSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { getEventBus } from "../shared/events/index.js";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ export async function loadModules(app) {
9
+ const modulesDir = join(__dirname, "../modules");
10
+ if (!existsSync(modulesDir)) return;
11
+
12
+ const moduleContext = { eventBus: getEventBus() };
13
+ const names = readdirSync(modulesDir, { withFileTypes: true })
14
+ .filter((d) => d.isDirectory())
15
+ .filter((d) => !d.name.startsWith("_"))
16
+ .filter((d) => !d.name.startsWith("."))
17
+ .map((d) => d.name);
18
+
19
+ for (const name of names) {
20
+ const moduleEntry = join(modulesDir, name, "index.js");
21
+ if (!existsSync(moduleEntry)) continue;
22
+
23
+ try {
24
+ const mod = await import(`../modules/${name}/index.js`);
25
+ if (typeof mod.register === "function") {
26
+ mod.register(app, moduleContext);
27
+ console.log(`✓ Module loaded: ${name}`);
28
+ } else {
29
+ console.warn(`! Module ignored (missing register): ${name}`);
30
+ }
31
+ } catch (error) {
32
+ console.error(`✗ Module failed: ${name} —`, error.message);
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,24 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import dotenv from "dotenv";
4
+ import { loadModules } from "./module-loader.js";
5
+ import { errorHandler } from "../shared/http/errors.js";
6
+
7
+ dotenv.config();
8
+
9
+ const app = express();
10
+ app.use(cors());
11
+ app.use(express.json({ limit: "10mb" }));
12
+
13
+ await loadModules(app);
14
+
15
+ app.get("/api/health", (_, res) => {
16
+ res.json({ status: "ok", loadedAt: new Date().toISOString() });
17
+ });
18
+
19
+ app.use(errorHandler);
20
+
21
+ const port = process.env.PORT || 3001;
22
+ app.listen(port, () => {
23
+ console.log(`Backend running on port ${port}`);
24
+ });
File without changes
@@ -0,0 +1,11 @@
1
+ # Reference (example module)
2
+
3
+ **Not loaded at runtime** — folder name starts with `_`. Copy patterns from here or use `npm run new:module`.
4
+
5
+ See [Module internal contract](../../../docs/architecture/MODULE_INTERNAL_CONTRACT.md).
6
+
7
+ ## Layout
8
+
9
+ `routes` → `services` → `repositories` / `domain` / `adapters`
10
+
11
+ `prompts` + `evals` for AI workflows. `tests/` for unit and integration coverage.
@@ -0,0 +1,3 @@
1
+ # Adapters — _reference
2
+
3
+ Wrappers for external systems (courts, e-file, storage, LLM providers).
@@ -0,0 +1,4 @@
1
+ export const moduleConfig = {
2
+ name: "_reference",
3
+ label: "_reference"
4
+ };
@@ -0,0 +1,3 @@
1
+ # Domain — _reference
2
+
3
+ Pure entities, value objects, and domain rules. No Express, DB, or HTTP imports.
@@ -0,0 +1,6 @@
1
+ # Evals — _reference
2
+
3
+ - **datasets/** — fixtures (input, expected constraints).
4
+ - **runners/** — `*.eval.mjs` files executed via `npm run test:evals`.
5
+
6
+ Run: `npm run test:evals -- _reference`
@@ -0,0 +1,12 @@
1
+ {
2
+ "cases": [
3
+ {
4
+ "id": "health-shape",
5
+ "description": "Health payload includes module name",
6
+ "input": {},
7
+ "expect": {
8
+ "status": "ok"
9
+ }
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,25 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "fs";
4
+ import { join, dirname } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { getHealth } from "../../services/health.service.js";
7
+ import { renderPrompt } from "../../../../shared/ai/prompt-registry.js";
8
+ import * as examplePrompt from "../../prompts/templates/example.prompt.js";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+
12
+ test("_reference: health service matches dataset", () => {
13
+ const dataset = JSON.parse(
14
+ readFileSync(join(__dirname, "../datasets/example.cases.json"), "utf8")
15
+ );
16
+ const expected = dataset.cases[0].expect;
17
+ const result = getHealth({ name: "_reference" });
18
+ assert.equal(result.status, expected.status);
19
+ assert.equal(result.module, "_reference");
20
+ });
21
+
22
+ test("_reference: example prompt renders variables", () => {
23
+ const rendered = renderPrompt(examplePrompt.template, { matterId: "MAT-001" });
24
+ assert.match(rendered, /MAT-001/);
25
+ });
@@ -0,0 +1,4 @@
1
+ export function registerModuleEvents(context) {
2
+ // context.eventBus.on("some:event", handler);
3
+ context.eventBus.emit("module:registered", { module: "_reference" });
4
+ }
@@ -0,0 +1,9 @@
1
+ import { createModuleRouter } from "./routes/index.js";
2
+ import { registerModuleEvents } from "./events/index.js";
3
+ import { moduleConfig } from "./config/index.js";
4
+
5
+ export function register(app, context) {
6
+ const router = createModuleRouter({ config: moduleConfig, context });
7
+ app.use("/api/_reference", router);
8
+ registerModuleEvents(context);
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "module": "_reference",
3
+ "prompts": [
4
+ {
5
+ "id": "example-assistant",
6
+ "version": "1.0.0",
7
+ "file": "templates/example.prompt.js",
8
+ "description": "Example prompt for ${title}",
9
+ "variables": [
10
+ "matterId"
11
+ ]
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,7 @@
1
+ export const id = "example-assistant";
2
+ export const version = "1.0.0";
3
+ export const variables = ["matterId"];
4
+
5
+ export const template = `You are a legal workflow assistant for module _reference.
6
+ Matter id: {{matterId}}
7
+ Respond with structured JSON only.`;
@@ -0,0 +1,10 @@
1
+ import { Router } from "express";
2
+ import { getHealth } from "../services/health.service.js";
3
+
4
+ export function createHealthRoutes({ config }) {
5
+ const router = Router();
6
+ router.get("/health", (_req, res) => {
7
+ res.json(getHealth(config));
8
+ });
9
+ return router;
10
+ }
@@ -0,0 +1,8 @@
1
+ import { Router } from "express";
2
+ import { createHealthRoutes } from "./health.routes.js";
3
+
4
+ export function createModuleRouter({ config, context }) {
5
+ const router = Router();
6
+ router.use(createHealthRoutes({ config, context }));
7
+ return router;
8
+ }
@@ -0,0 +1,8 @@
1
+ export function isHealthResponse(value) {
2
+ return Boolean(
3
+ value &&
4
+ typeof value.module === "string" &&
5
+ typeof value.status === "string" &&
6
+ typeof value.timestamp === "string"
7
+ );
8
+ }
@@ -0,0 +1,7 @@
1
+ export function getHealth(config) {
2
+ return {
3
+ module: config.name,
4
+ status: "ok",
5
+ timestamp: new Date().toISOString()
6
+ };
7
+ }
@@ -0,0 +1,20 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createTestApp } from "../../../../shared/testing/create-test-app.js";
4
+ import { register } from "../../index.js";
5
+
6
+ test("GET /api/_reference/health", async () => {
7
+ const app = createTestApp(register);
8
+ const server = app.listen(0);
9
+ const { port } = server.address();
10
+
11
+ try {
12
+ const res = await fetch(`http://127.0.0.1:${port}/api/_reference/health`);
13
+ assert.equal(res.status, 200);
14
+ const body = await res.json();
15
+ assert.equal(body.module, "_reference");
16
+ assert.equal(body.status, "ok");
17
+ } finally {
18
+ server.close();
19
+ }
20
+ });
@@ -0,0 +1,9 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { getHealth } from "../../services/health.service.js";
4
+
5
+ test("getHealth returns module metadata", () => {
6
+ const result = getHealth({ name: "_reference" });
7
+ assert.equal(result.module, "_reference");
8
+ assert.equal(result.status, "ok");
9
+ });
@@ -0,0 +1,3 @@
1
+ export function moduleSlug(value) {
2
+ return String(value ?? "").trim().toLowerCase();
3
+ }
@@ -0,0 +1,42 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath, pathToFileURL } from "url";
4
+
5
+ /**
6
+ * Load a versioned prompt template from a module's prompts/ folder.
7
+ * @param {string} moduleDir - absolute path to backend/src/modules/<name>
8
+ * @param {string} promptId - id matching prompts/templates and manifest
9
+ */
10
+ export async function loadPromptTemplate(moduleDir, promptId) {
11
+ const manifestPath = join(moduleDir, "prompts", "manifest.json");
12
+ if (!existsSync(manifestPath)) {
13
+ throw new Error(`Prompt manifest not found: ${manifestPath}`);
14
+ }
15
+
16
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
17
+ const entry = manifest.prompts?.find((p) => p.id === promptId);
18
+ if (!entry?.file) {
19
+ throw new Error(`Prompt id not in manifest: ${promptId}`);
20
+ }
21
+
22
+ const filePath = join(moduleDir, "prompts", entry.file);
23
+ const mod = await import(pathToFileURL(filePath).href);
24
+ return {
25
+ id: mod.id ?? promptId,
26
+ version: mod.version ?? entry.version ?? "0.0.0",
27
+ template: mod.template,
28
+ variables: mod.variables ?? entry.variables ?? []
29
+ };
30
+ }
31
+
32
+ export function renderPrompt(template, variables = {}) {
33
+ let output = template;
34
+ for (const [key, value] of Object.entries(variables)) {
35
+ output = output.replaceAll(`{{${key}}}`, String(value ?? ""));
36
+ }
37
+ return output;
38
+ }
39
+
40
+ export function moduleDirFromMeta(metaUrl) {
41
+ return dirname(fileURLToPath(metaUrl));
42
+ }
@@ -0,0 +1,8 @@
1
+ import { EventEmitter } from "events";
2
+
3
+ const bus = new EventEmitter();
4
+ bus.setMaxListeners(100);
5
+
6
+ export function getEventBus() {
7
+ return bus;
8
+ }
@@ -0,0 +1,10 @@
1
+ export function errorHandler(error, _req, res, _next) {
2
+ const status = Number.isInteger(error?.status) ? error.status : 500;
3
+ const message = error?.message || "Internal Server Error";
4
+
5
+ if (status >= 500) {
6
+ console.error("Unhandled error:", error);
7
+ }
8
+
9
+ res.status(status).json({ error: message });
10
+ }
@@ -0,0 +1,13 @@
1
+ import express from "express";
2
+
3
+ /**
4
+ * Minimal Express app for module integration tests.
5
+ * Pass a register function from the module under test.
6
+ */
7
+ export function createTestApp(register) {
8
+ const app = express();
9
+ app.use(express.json());
10
+ const context = { eventBus: { emit() {}, on() {} } };
11
+ register(app, context);
12
+ return app;
13
+ }