@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.
- package/README.md +43 -0
- package/bin/create-modular-monolith.js +132 -0
- package/package.json +39 -0
- package/template/README.md +73 -0
- package/template/backend/package-lock.json +882 -0
- package/template/backend/package.json +20 -0
- package/template/backend/scripts/check-module-boundaries.mjs +69 -0
- package/template/backend/scripts/check-module-layers.mjs +152 -0
- package/template/backend/src/core/module-loader.js +35 -0
- package/template/backend/src/core/server.js +24 -0
- package/template/backend/src/modules/.gitkeep +0 -0
- package/template/backend/src/modules/_reference/README.md +11 -0
- package/template/backend/src/modules/_reference/adapters/README.md +3 -0
- package/template/backend/src/modules/_reference/config/index.js +4 -0
- package/template/backend/src/modules/_reference/domain/README.md +3 -0
- package/template/backend/src/modules/_reference/evals/README.md +6 -0
- package/template/backend/src/modules/_reference/evals/datasets/example.cases.json +12 -0
- package/template/backend/src/modules/_reference/evals/runners/example.eval.mjs +25 -0
- package/template/backend/src/modules/_reference/events/index.js +4 -0
- package/template/backend/src/modules/_reference/index.js +9 -0
- package/template/backend/src/modules/_reference/prompts/manifest.json +14 -0
- package/template/backend/src/modules/_reference/prompts/templates/example.prompt.js +7 -0
- package/template/backend/src/modules/_reference/repositories/.gitkeep +0 -0
- package/template/backend/src/modules/_reference/routes/health.routes.js +10 -0
- package/template/backend/src/modules/_reference/routes/index.js +8 -0
- package/template/backend/src/modules/_reference/schemas/health.schema.js +8 -0
- package/template/backend/src/modules/_reference/services/health.service.js +7 -0
- package/template/backend/src/modules/_reference/tests/integration/health.routes.test.js +20 -0
- package/template/backend/src/modules/_reference/tests/unit/health.service.test.js +9 -0
- package/template/backend/src/modules/_reference/utils/index.js +3 -0
- package/template/backend/src/shared/ai/prompt-registry.js +42 -0
- package/template/backend/src/shared/events/index.js +8 -0
- package/template/backend/src/shared/http/errors.js +10 -0
- package/template/backend/src/shared/testing/create-test-app.js +13 -0
- package/template/docs/DEVLOG_V2.md +369 -0
- package/template/docs/PUBLISHING.md +39 -0
- package/template/docs/README.md +13 -0
- package/template/docs/STARTER_PACK.md +98 -0
- package/template/docs/architecture/ARCHITECTURE_GUARDRAILS.md +74 -0
- package/template/docs/architecture/MODULE_INTERNAL_CONTRACT.md +164 -0
- package/template/frontend/index.html +12 -0
- package/template/frontend/package-lock.json +1724 -0
- package/template/frontend/package.json +21 -0
- package/template/frontend/src/core/App.jsx +35 -0
- package/template/frontend/src/core/moduleRegistry.jsx +39 -0
- package/template/frontend/src/index.css +53 -0
- package/template/frontend/src/main.jsx +10 -0
- package/template/frontend/src/modules/.gitkeep +0 -0
- package/template/frontend/src/modules/_reference/README.md +3 -0
- package/template/frontend/src/modules/_reference/components/ModuleHealthCard.jsx +14 -0
- package/template/frontend/src/modules/_reference/hooks/use-module-health.js +27 -0
- package/template/frontend/src/modules/_reference/index.jsx +7 -0
- package/template/frontend/src/modules/_reference/pages/_referencePage.jsx +11 -0
- package/template/frontend/src/modules/_reference/prompts/README.md +3 -0
- package/template/frontend/src/modules/_reference/schemas/health.schema.js +3 -0
- package/template/frontend/src/modules/_reference/services/health-api.js +5 -0
- package/template/frontend/src/modules/_reference/tests/unit/health.schema.test.js +8 -0
- package/template/frontend/src/modules/_reference/utils/index.js +3 -0
- package/template/frontend/src/shared/api/client.js +10 -0
- package/template/frontend/vite.config.js +6 -0
- package/template/package.json +16 -0
- package/template/scripts/lib/module-scaffold.mjs +409 -0
- package/template/scripts/new-module.mjs +58 -0
- package/template/scripts/run-module-evals.mjs +43 -0
- package/template/scripts/sync-cli-template.mjs +44 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
export function toTitleCase(value) {
|
|
2
|
+
return value
|
|
3
|
+
.split("-")
|
|
4
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
5
|
+
.join(" ");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function toComponentName(moduleName) {
|
|
9
|
+
return moduleName
|
|
10
|
+
.split("-")
|
|
11
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
12
|
+
.join("");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getBackendFiles(moduleName) {
|
|
16
|
+
const title = toTitleCase(moduleName);
|
|
17
|
+
const files = [];
|
|
18
|
+
|
|
19
|
+
const add = (rel, content) => files.push({ rel, content });
|
|
20
|
+
|
|
21
|
+
add("index.js", `import { createModuleRouter } from "./routes/index.js";
|
|
22
|
+
import { registerModuleEvents } from "./events/index.js";
|
|
23
|
+
import { moduleConfig } from "./config/index.js";
|
|
24
|
+
|
|
25
|
+
export function register(app, context) {
|
|
26
|
+
const router = createModuleRouter({ config: moduleConfig, context });
|
|
27
|
+
app.use("/api/${moduleName}", router);
|
|
28
|
+
registerModuleEvents(context);
|
|
29
|
+
}
|
|
30
|
+
`);
|
|
31
|
+
|
|
32
|
+
add("config/index.js", `export const moduleConfig = {
|
|
33
|
+
name: "${moduleName}",
|
|
34
|
+
label: "${title}"
|
|
35
|
+
};
|
|
36
|
+
`);
|
|
37
|
+
|
|
38
|
+
add(
|
|
39
|
+
"routes/index.js",
|
|
40
|
+
`import { Router } from "express";
|
|
41
|
+
import { createHealthRoutes } from "./health.routes.js";
|
|
42
|
+
|
|
43
|
+
export function createModuleRouter({ config, context }) {
|
|
44
|
+
const router = Router();
|
|
45
|
+
router.use(createHealthRoutes({ config, context }));
|
|
46
|
+
return router;
|
|
47
|
+
}
|
|
48
|
+
`
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
add(
|
|
52
|
+
"routes/health.routes.js",
|
|
53
|
+
`import { Router } from "express";
|
|
54
|
+
import { getHealth } from "../services/health.service.js";
|
|
55
|
+
|
|
56
|
+
export function createHealthRoutes({ config }) {
|
|
57
|
+
const router = Router();
|
|
58
|
+
router.get("/health", (_req, res) => {
|
|
59
|
+
res.json(getHealth(config));
|
|
60
|
+
});
|
|
61
|
+
return router;
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
add(
|
|
67
|
+
"services/health.service.js",
|
|
68
|
+
`export function getHealth(config) {
|
|
69
|
+
return {
|
|
70
|
+
module: config.name,
|
|
71
|
+
status: "ok",
|
|
72
|
+
timestamp: new Date().toISOString()
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
add("repositories/.gitkeep", "");
|
|
79
|
+
add(
|
|
80
|
+
"domain/README.md",
|
|
81
|
+
`# Domain — ${title}
|
|
82
|
+
|
|
83
|
+
Pure entities, value objects, and domain rules. No Express, DB, or HTTP imports.
|
|
84
|
+
`
|
|
85
|
+
);
|
|
86
|
+
add(
|
|
87
|
+
"adapters/README.md",
|
|
88
|
+
`# Adapters — ${title}
|
|
89
|
+
|
|
90
|
+
Wrappers for external systems (courts, e-file, storage, LLM providers).
|
|
91
|
+
`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
add(
|
|
95
|
+
"events/index.js",
|
|
96
|
+
`export function registerModuleEvents(context) {
|
|
97
|
+
// context.eventBus.on("some:event", handler);
|
|
98
|
+
context.eventBus.emit("module:registered", { module: "${moduleName}" });
|
|
99
|
+
}
|
|
100
|
+
`
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
add(
|
|
104
|
+
"schemas/health.schema.js",
|
|
105
|
+
`export function isHealthResponse(value) {
|
|
106
|
+
return (
|
|
107
|
+
value &&
|
|
108
|
+
typeof value.module === "string" &&
|
|
109
|
+
typeof value.status === "string" &&
|
|
110
|
+
typeof value.timestamp === "string"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
`
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
add(
|
|
117
|
+
"utils/index.js",
|
|
118
|
+
`export function moduleSlug(value) {
|
|
119
|
+
return String(value ?? "").trim().toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
`
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
add(
|
|
125
|
+
"prompts/manifest.json",
|
|
126
|
+
JSON.stringify(
|
|
127
|
+
{
|
|
128
|
+
module: moduleName,
|
|
129
|
+
prompts: [
|
|
130
|
+
{
|
|
131
|
+
id: "example-assistant",
|
|
132
|
+
version: "1.0.0",
|
|
133
|
+
file: "templates/example.prompt.js",
|
|
134
|
+
description: "Example prompt for ${title}",
|
|
135
|
+
variables: ["matterId"]
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
null,
|
|
140
|
+
2
|
|
141
|
+
) + "\n"
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
add(
|
|
145
|
+
"prompts/templates/example.prompt.js",
|
|
146
|
+
`export const id = "example-assistant";
|
|
147
|
+
export const version = "1.0.0";
|
|
148
|
+
export const variables = ["matterId"];
|
|
149
|
+
|
|
150
|
+
export const template = \`You are a legal workflow assistant for module ${moduleName}.
|
|
151
|
+
Matter id: {{matterId}}
|
|
152
|
+
Respond with structured JSON only.\`;
|
|
153
|
+
`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
add(
|
|
157
|
+
"evals/datasets/example.cases.json",
|
|
158
|
+
JSON.stringify(
|
|
159
|
+
{
|
|
160
|
+
cases: [
|
|
161
|
+
{
|
|
162
|
+
id: "health-shape",
|
|
163
|
+
description: "Health payload includes module name",
|
|
164
|
+
input: {},
|
|
165
|
+
expect: { status: "ok" }
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
null,
|
|
170
|
+
2
|
|
171
|
+
) + "\n"
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
add(
|
|
175
|
+
"evals/runners/example.eval.mjs",
|
|
176
|
+
`import { test } from "node:test";
|
|
177
|
+
import assert from "node:assert/strict";
|
|
178
|
+
import { readFileSync } from "fs";
|
|
179
|
+
import { join, dirname } from "path";
|
|
180
|
+
import { fileURLToPath } from "url";
|
|
181
|
+
import { getHealth } from "../../services/health.service.js";
|
|
182
|
+
import { renderPrompt } from "../../../../shared/ai/prompt-registry.js";
|
|
183
|
+
import * as examplePrompt from "../../prompts/templates/example.prompt.js";
|
|
184
|
+
|
|
185
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
186
|
+
|
|
187
|
+
test("${moduleName}: health service matches dataset", () => {
|
|
188
|
+
const dataset = JSON.parse(
|
|
189
|
+
readFileSync(join(__dirname, "../datasets/example.cases.json"), "utf8")
|
|
190
|
+
);
|
|
191
|
+
const expected = dataset.cases[0].expect;
|
|
192
|
+
const result = getHealth({ name: "${moduleName}" });
|
|
193
|
+
assert.equal(result.status, expected.status);
|
|
194
|
+
assert.equal(result.module, "${moduleName}");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("${moduleName}: example prompt renders variables", () => {
|
|
198
|
+
const rendered = renderPrompt(examplePrompt.template, { matterId: "MAT-001" });
|
|
199
|
+
assert.match(rendered, /MAT-001/);
|
|
200
|
+
});
|
|
201
|
+
`
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
add(
|
|
205
|
+
"evals/README.md",
|
|
206
|
+
`# Evals — ${title}
|
|
207
|
+
|
|
208
|
+
- **datasets/** — fixtures (input, expected constraints).
|
|
209
|
+
- **runners/** — \`*.eval.mjs\` files executed via \`npm run test:evals\`.
|
|
210
|
+
|
|
211
|
+
Run: \`npm run test:evals -- ${moduleName}\`
|
|
212
|
+
`
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
add(
|
|
216
|
+
"tests/unit/health.service.test.js",
|
|
217
|
+
`import { test } from "node:test";
|
|
218
|
+
import assert from "node:assert/strict";
|
|
219
|
+
import { getHealth } from "../../services/health.service.js";
|
|
220
|
+
|
|
221
|
+
test("getHealth returns module metadata", () => {
|
|
222
|
+
const result = getHealth({ name: "${moduleName}" });
|
|
223
|
+
assert.equal(result.module, "${moduleName}");
|
|
224
|
+
assert.equal(result.status, "ok");
|
|
225
|
+
});
|
|
226
|
+
`
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
add(
|
|
230
|
+
"tests/integration/health.routes.test.js",
|
|
231
|
+
`import { test } from "node:test";
|
|
232
|
+
import assert from "node:assert/strict";
|
|
233
|
+
import { createTestApp } from "../../../../shared/testing/create-test-app.js";
|
|
234
|
+
import { register } from "../../index.js";
|
|
235
|
+
|
|
236
|
+
test("GET /api/${moduleName}/health", async () => {
|
|
237
|
+
const app = createTestApp(register);
|
|
238
|
+
const server = app.listen(0);
|
|
239
|
+
const { port } = server.address();
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const res = await fetch(\`http://127.0.0.1:\${port}/api/${moduleName}/health\`);
|
|
243
|
+
assert.equal(res.status, 200);
|
|
244
|
+
const body = await res.json();
|
|
245
|
+
assert.equal(body.module, "${moduleName}");
|
|
246
|
+
assert.equal(body.status, "ok");
|
|
247
|
+
} finally {
|
|
248
|
+
server.close();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
`
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
add(
|
|
255
|
+
"README.md",
|
|
256
|
+
`# ${title}
|
|
257
|
+
|
|
258
|
+
See [Module internal contract](../../../docs/architecture/MODULE_INTERNAL_CONTRACT.md).
|
|
259
|
+
|
|
260
|
+
## Layout
|
|
261
|
+
|
|
262
|
+
\`routes\` → \`services\` → \`repositories\` / \`domain\` / \`adapters\`
|
|
263
|
+
|
|
264
|
+
\`prompts\` + \`evals\` for AI workflows. \`tests/\` for unit and integration coverage.
|
|
265
|
+
`
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
return files;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function getFrontendFiles(moduleName, label) {
|
|
272
|
+
const componentName = toComponentName(moduleName);
|
|
273
|
+
const files = [];
|
|
274
|
+
|
|
275
|
+
const add = (rel, content) => files.push({ rel, content });
|
|
276
|
+
|
|
277
|
+
add(
|
|
278
|
+
"index.jsx",
|
|
279
|
+
`import { ${componentName}Page } from "./pages/${componentName}Page.jsx";
|
|
280
|
+
|
|
281
|
+
export default {
|
|
282
|
+
route: "/${moduleName}",
|
|
283
|
+
label: "${label}",
|
|
284
|
+
Component: ${componentName}Page
|
|
285
|
+
};
|
|
286
|
+
`
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
add(
|
|
290
|
+
`pages/${componentName}Page.jsx`,
|
|
291
|
+
`import { ModuleHealthCard } from "../components/ModuleHealthCard.jsx";
|
|
292
|
+
|
|
293
|
+
export function ${componentName}Page() {
|
|
294
|
+
return (
|
|
295
|
+
<section className="card">
|
|
296
|
+
<h2>${label}</h2>
|
|
297
|
+
<p className="muted">Module shell — extend pages, hooks, and services.</p>
|
|
298
|
+
<ModuleHealthCard />
|
|
299
|
+
</section>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
`
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
add(
|
|
306
|
+
"components/ModuleHealthCard.jsx",
|
|
307
|
+
`import { useModuleHealth } from "../hooks/use-module-health.js";
|
|
308
|
+
|
|
309
|
+
export function ModuleHealthCard() {
|
|
310
|
+
const { data, error, loading } = useModuleHealth();
|
|
311
|
+
|
|
312
|
+
if (loading) return <p className="muted">Checking backend…</p>;
|
|
313
|
+
if (error) return <p className="muted">Backend unavailable: {error.message}</p>;
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<p>
|
|
317
|
+
Backend health: <code>{data?.status}</code> ({data?.module})
|
|
318
|
+
</p>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
`
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
add(
|
|
325
|
+
"hooks/use-module-health.js",
|
|
326
|
+
`import { useEffect, useState } from "react";
|
|
327
|
+
import { fetchModuleHealth } from "../services/health-api.js";
|
|
328
|
+
|
|
329
|
+
export function useModuleHealth() {
|
|
330
|
+
const [data, setData] = useState(null);
|
|
331
|
+
const [error, setError] = useState(null);
|
|
332
|
+
const [loading, setLoading] = useState(true);
|
|
333
|
+
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
let active = true;
|
|
336
|
+
fetchModuleHealth()
|
|
337
|
+
.then((result) => {
|
|
338
|
+
if (active) setData(result);
|
|
339
|
+
})
|
|
340
|
+
.catch((err) => {
|
|
341
|
+
if (active) setError(err);
|
|
342
|
+
})
|
|
343
|
+
.finally(() => {
|
|
344
|
+
if (active) setLoading(false);
|
|
345
|
+
});
|
|
346
|
+
return () => {
|
|
347
|
+
active = false;
|
|
348
|
+
};
|
|
349
|
+
}, []);
|
|
350
|
+
|
|
351
|
+
return { data, error, loading };
|
|
352
|
+
}
|
|
353
|
+
`
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
add(
|
|
357
|
+
"services/health-api.js",
|
|
358
|
+
`import { apiGet } from "../../shared/api/client.js";
|
|
359
|
+
|
|
360
|
+
export function fetchModuleHealth() {
|
|
361
|
+
return apiGet("/api/${moduleName}/health");
|
|
362
|
+
}
|
|
363
|
+
`
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
add(
|
|
367
|
+
"schemas/health.schema.js",
|
|
368
|
+
`export function isHealthResponse(value) {
|
|
369
|
+
return Boolean(value && typeof value.status === "string");
|
|
370
|
+
}
|
|
371
|
+
`
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
add("utils/index.js", `export function formatModuleLabel(label) {
|
|
375
|
+
return label?.trim() || "Module";
|
|
376
|
+
}
|
|
377
|
+
`);
|
|
378
|
+
|
|
379
|
+
add(
|
|
380
|
+
"prompts/README.md",
|
|
381
|
+
`# UI prompts — ${label}
|
|
382
|
+
|
|
383
|
+
Optional: assistant copy, tool hints, and in-product AI instructions for this module.
|
|
384
|
+
`
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
add(
|
|
388
|
+
"tests/unit/health.schema.test.js",
|
|
389
|
+
`import { test } from "node:test";
|
|
390
|
+
import assert from "node:assert/strict";
|
|
391
|
+
import { isHealthResponse } from "../../schemas/health.schema.js";
|
|
392
|
+
|
|
393
|
+
test("isHealthResponse validates shape", () => {
|
|
394
|
+
assert.equal(isHealthResponse({ status: "ok" }), true);
|
|
395
|
+
assert.equal(isHealthResponse(null), false);
|
|
396
|
+
});
|
|
397
|
+
`
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
add(
|
|
401
|
+
"README.md",
|
|
402
|
+
`# ${label} (frontend)
|
|
403
|
+
|
|
404
|
+
See [Module internal contract](../../../docs/architecture/MODULE_INTERNAL_CONTRACT.md).
|
|
405
|
+
`
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
return files;
|
|
409
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync, existsSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import {
|
|
5
|
+
getBackendFiles,
|
|
6
|
+
getFrontendFiles,
|
|
7
|
+
toTitleCase
|
|
8
|
+
} from "./lib/module-scaffold.mjs";
|
|
9
|
+
|
|
10
|
+
const [name, ...rest] = process.argv.slice(2);
|
|
11
|
+
if (!name) {
|
|
12
|
+
console.error(
|
|
13
|
+
'Usage: node scripts/new-module.mjs <module-name> [--label "Module Label"]'
|
|
14
|
+
);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
19
|
+
console.error("Module name must be kebab-case (example: intake-triage)");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const labelFlagIndex = rest.indexOf("--label");
|
|
24
|
+
const label =
|
|
25
|
+
labelFlagIndex >= 0 && rest[labelFlagIndex + 1]
|
|
26
|
+
? rest[labelFlagIndex + 1]
|
|
27
|
+
: toTitleCase(name);
|
|
28
|
+
|
|
29
|
+
const root = new URL("../", import.meta.url).pathname;
|
|
30
|
+
const backendDir = join(root, "backend/src/modules", name);
|
|
31
|
+
const frontendDir = join(root, "frontend/src/modules", name);
|
|
32
|
+
|
|
33
|
+
if (existsSync(backendDir) || existsSync(frontendDir)) {
|
|
34
|
+
console.error(`Module already exists: ${name}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeTree(baseDir, files) {
|
|
39
|
+
mkdirSync(baseDir, { recursive: true });
|
|
40
|
+
for (const { rel, content } of files) {
|
|
41
|
+
const target = join(baseDir, rel);
|
|
42
|
+
mkdirSync(join(target, ".."), { recursive: true });
|
|
43
|
+
writeFileSync(target, content, "utf8");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
writeTree(backendDir, getBackendFiles(name));
|
|
48
|
+
writeTree(frontendDir, getFrontendFiles(name, label));
|
|
49
|
+
|
|
50
|
+
console.log(`Created module: ${name}`);
|
|
51
|
+
console.log(` backend/src/modules/${name}/`);
|
|
52
|
+
console.log(` frontend/src/modules/${name}/`);
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log("Next:");
|
|
55
|
+
console.log(" npm run lint:architecture");
|
|
56
|
+
console.log(` npm test`);
|
|
57
|
+
console.log(` npm run test:evals -- ${name}`);
|
|
58
|
+
console.log(" Restart backend and refresh frontend");
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
|
|
6
|
+
const moduleName = process.argv[2];
|
|
7
|
+
const root = new URL("../", import.meta.url).pathname;
|
|
8
|
+
const modulesDir = join(root, "backend/src/modules");
|
|
9
|
+
|
|
10
|
+
if (!existsSync(modulesDir)) {
|
|
11
|
+
console.error("No modules directory.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const targets = moduleName
|
|
16
|
+
? [moduleName]
|
|
17
|
+
: readdirSync(modulesDir, { withFileTypes: true })
|
|
18
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("_"))
|
|
19
|
+
.map((d) => d.name);
|
|
20
|
+
|
|
21
|
+
let failed = false;
|
|
22
|
+
|
|
23
|
+
for (const name of targets) {
|
|
24
|
+
const runnersDir = join(modulesDir, name, "evals", "runners");
|
|
25
|
+
if (!existsSync(runnersDir)) continue;
|
|
26
|
+
|
|
27
|
+
const runners = readdirSync(runnersDir).filter(
|
|
28
|
+
(f) => f.endsWith(".eval.mjs") || f.endsWith(".eval.js")
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
for (const runner of runners) {
|
|
32
|
+
const file = join(runnersDir, runner);
|
|
33
|
+
console.log(`\n▶ eval ${name}/${runner}`);
|
|
34
|
+
const result = spawnSync(process.execPath, ["--test", file], {
|
|
35
|
+
stdio: "inherit",
|
|
36
|
+
cwd: join(root, "backend")
|
|
37
|
+
});
|
|
38
|
+
if (result.status !== 0) failed = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (failed) process.exit(1);
|
|
43
|
+
console.log("\nEvals complete.");
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copies the v2 starter into packages/create-modular-monolith/template
|
|
4
|
+
* for publishing with the npm CLI.
|
|
5
|
+
*/
|
|
6
|
+
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
|
|
9
|
+
const root = new URL("../", import.meta.url).pathname;
|
|
10
|
+
const target = join(root, "packages/create-modular-monolith/template");
|
|
11
|
+
|
|
12
|
+
const COPY_ROOTS = ["backend", "frontend", "docs", "scripts", "README.md", ".gitignore", "package.json"];
|
|
13
|
+
|
|
14
|
+
const EXCLUDE_DIRS = new Set([
|
|
15
|
+
"node_modules",
|
|
16
|
+
".git",
|
|
17
|
+
"dist",
|
|
18
|
+
"coverage",
|
|
19
|
+
"packages"
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
if (existsSync(target)) {
|
|
23
|
+
rmSync(target, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
mkdirSync(target, { recursive: true });
|
|
26
|
+
|
|
27
|
+
for (const item of COPY_ROOTS) {
|
|
28
|
+
const src = join(root, item);
|
|
29
|
+
if (!existsSync(src)) {
|
|
30
|
+
console.warn(`Skip missing: ${item}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const dest = join(target, item);
|
|
34
|
+
cpSync(src, dest, {
|
|
35
|
+
recursive: true,
|
|
36
|
+
filter: (sourcePath) => {
|
|
37
|
+
const parts = sourcePath.split(/[/\\]/);
|
|
38
|
+
return !parts.some((part) => EXCLUDE_DIRS.has(part));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
console.log(`✓ ${item}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(`\nTemplate synced to ${target}`);
|