@zaaxch/tailframe 1.0.0 → 2.1.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/package.json +1 -1
- package/src/architecture.mjs +4 -0
- package/src/conventions.mjs +84 -0
- package/src/generate.mjs +118 -57
- package/src/new.mjs +197 -134
- package/src/service-templates.mjs +483 -0
- package/src/ui-templates.mjs +141 -0
- package/src/validate.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zaaxch/tailframe",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/architecture.mjs
CHANGED
|
@@ -148,6 +148,10 @@ function validateStructure(root, kind, fail) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
function validateServiceImport(source, target, external, fail, graph) {
|
|
151
|
+
if (external === "tsyringe" && source !== "src/app/container.ts") {
|
|
152
|
+
fail(`${source} may not import tsyringe; dependency-injection framework code is confined to src/app/container.ts`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
151
155
|
if (source === "src/server.ts" || source === "src/worker.ts") {
|
|
152
156
|
if (external) {
|
|
153
157
|
if (external !== "reflect-metadata") fail(`${source} may import only reflect-metadata and app bootstrap code, not ${external}`);
|
package/src/conventions.mjs
CHANGED
|
@@ -15,6 +15,11 @@ const ROUTE_NAMES_CANONICAL_DECLARATION = /\bexport\s+enum\s+RouteNames\b/;
|
|
|
15
15
|
const ROUTE_NAMES_VALUE = /^\s*[A-Z][A-Z0-9_]*\s*=\s*["']([^"']+)["']/gm;
|
|
16
16
|
const ROUTE_NAME_REFERENCE = /\bname:\s*["']([^"']+)["']/g;
|
|
17
17
|
|
|
18
|
+
const camel = (kebab) => {
|
|
19
|
+
const value = kebab.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("");
|
|
20
|
+
return value[0].toLowerCase() + value.slice(1);
|
|
21
|
+
};
|
|
22
|
+
|
|
18
23
|
function parseName(fileName) {
|
|
19
24
|
if (fileName.endsWith(".d.ts")) return undefined;
|
|
20
25
|
const extension = path.extname(fileName);
|
|
@@ -98,6 +103,9 @@ export function validateConventions(rootArgument, kind) {
|
|
|
98
103
|
const layer = remainder.split("/")[0];
|
|
99
104
|
if (kind === "service") validateServiceFile(relative, moduleName, layer, remainder, name, flag);
|
|
100
105
|
else validateUiFile(relative, moduleName, layer, name, flag);
|
|
106
|
+
if (kind === "service" && name.extension === ".ts") {
|
|
107
|
+
validateServiceSource(relative, moduleName, layer, remainder, name, fs.readFileSync(absolute, "utf8"), flag);
|
|
108
|
+
}
|
|
101
109
|
|
|
102
110
|
if (kind === "service" && name.extension === ".ts" && relative !== `${modulePath}/domain/identifiers.ts` &&
|
|
103
111
|
BRAND_PATTERN.test(fs.readFileSync(absolute, "utf8"))) {
|
|
@@ -107,10 +115,86 @@ export function validateConventions(rootArgument, kind) {
|
|
|
107
115
|
if (kind === "ui") {
|
|
108
116
|
validateRouteNames(root, src, flag);
|
|
109
117
|
validateCanonicalShell(root, flag);
|
|
118
|
+
} else {
|
|
119
|
+
validateServiceComposition(root, flag);
|
|
110
120
|
}
|
|
111
121
|
return violations;
|
|
112
122
|
}
|
|
113
123
|
|
|
124
|
+
function validateServiceSource(relative, moduleName, layer, remainder, name, source, flag) {
|
|
125
|
+
if (layer === "use-cases" && remainder.split("/")[1] !== "ports") {
|
|
126
|
+
const exportedClasses = [...source.matchAll(/^export\s+class\s+([A-Z][A-Za-z0-9]*)\b/gm)].map((match) => match[1]);
|
|
127
|
+
if (exportedClasses.length > 1) {
|
|
128
|
+
flag("G5", relative, `${relative} exports multiple classes (${exportedClasses.join(", ")}); keep one named use-case class per file`);
|
|
129
|
+
} else if (exportedClasses.length === 1 && exportedClasses[0] !== name.stem) {
|
|
130
|
+
flag("G1", relative, `${relative} must be named ${exportedClasses[0]}.ts after its exported use-case class`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (layer !== "http") return;
|
|
135
|
+
if (name.role === "routes" && name.subject === moduleName) {
|
|
136
|
+
const factories = [...source.matchAll(/^export\s+function\s+([A-Za-z][A-Za-z0-9]*Routes)\s*\(/gm)].map((match) => match[1]);
|
|
137
|
+
const expected = `${camel(moduleName)}Routes`;
|
|
138
|
+
if (factories.length !== 1 || factories[0] !== expected) {
|
|
139
|
+
flag("S8", relative, `${relative} must export exactly one module route factory named ${expected}`);
|
|
140
|
+
}
|
|
141
|
+
const postCount = [...source.matchAll(/\brouter\.post\s*\(/g)].length;
|
|
142
|
+
const rpcHandlerCount = [...source.matchAll(/\brpcHandler\s*\(/g)].length;
|
|
143
|
+
if (postCount > rpcHandlerCount) {
|
|
144
|
+
flag("S8", relative, `${relative} must translate every RPC POST through rpcHandler; found ${postCount} POST routes and ${rpcHandlerCount} rpcHandler calls`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (name.role === "schemas" && name.subject === moduleName) {
|
|
148
|
+
const schemas = [...source.matchAll(/^export\s+const\s+([A-Za-z][A-Za-z0-9]*)\s*=/gm)].map((match) => match[1]);
|
|
149
|
+
if (schemas.length === 0) flag("S8", relative, `${relative} must export at least one PascalCase operation schema`);
|
|
150
|
+
for (const schema of schemas) {
|
|
151
|
+
if (!PASCAL.test(schema) || !schema.endsWith("Schema")) {
|
|
152
|
+
flag("S8", relative, `${relative} exports ${schema}; operation schemas are PascalCase and end with Schema`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validateServiceComposition(root, flag) {
|
|
159
|
+
const containerRelative = "src/app/container.ts";
|
|
160
|
+
const containerFile = path.join(root, containerRelative);
|
|
161
|
+
if (!fs.existsSync(containerFile)) {
|
|
162
|
+
flag("S9", containerRelative, `${containerRelative} is required as the explicit application composition root`);
|
|
163
|
+
} else {
|
|
164
|
+
const source = fs.readFileSync(containerFile, "utf8");
|
|
165
|
+
for (const [pattern, message] of [
|
|
166
|
+
[/\bexport\s+function\s+registerDependencies\s*\(/, "must export registerDependencies"],
|
|
167
|
+
[/\bcontainer\.reset\s*\(/, "must reset the container before registration"],
|
|
168
|
+
[/\bregisterInstance\s*\(/, "must register explicit instances"]
|
|
169
|
+
]) {
|
|
170
|
+
if (!pattern.test(source)) flag("S9", containerRelative, `${containerRelative} ${message}`);
|
|
171
|
+
}
|
|
172
|
+
if (/\bregisterSingleton\s*\(|\binstanceCachingFactory\s*\(|@inject(?:able)?\b/.test(source)) {
|
|
173
|
+
flag("S9", containerRelative, `${containerRelative} must use explicit construction and instance registration, not decorators, singleton factories, or implicit resolution`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const serverRelative = "src/app/server.ts";
|
|
178
|
+
const serverFile = path.join(root, serverRelative);
|
|
179
|
+
if (!fs.existsSync(serverFile)) {
|
|
180
|
+
flag("S9", serverRelative, `${serverRelative} is required as the HTTP application assembly`);
|
|
181
|
+
} else if (!/\bregisterDependencies\s*\(/.test(fs.readFileSync(serverFile, "utf8"))) {
|
|
182
|
+
flag("S9", serverRelative, `${serverRelative} must initialize the app-owned composition root after infrastructure connects`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const workerRelative = "src/app/workers/startWorker.ts";
|
|
186
|
+
const workerFile = path.join(root, workerRelative);
|
|
187
|
+
if (fs.existsSync(workerFile)) {
|
|
188
|
+
const source = fs.readFileSync(workerFile, "utf8");
|
|
189
|
+
if (!/\bregisterDependencies\s*\(/.test(source)) {
|
|
190
|
+
flag("S9", workerRelative, `${workerRelative} must initialize the same app-owned composition root as the HTTP server`);
|
|
191
|
+
}
|
|
192
|
+
if (/\bprocess\.exit\s*\(/.test(source)) {
|
|
193
|
+
flag("S9", workerRelative, `${workerRelative} must return an awaitable shutdown path instead of forcing process.exit`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
114
198
|
function normalizeGeneratedSource(source) {
|
|
115
199
|
return source.replace(/\r\n/g, "\n");
|
|
116
200
|
}
|
package/src/generate.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { loadExceptions, isExcepted } from "./exceptions.mjs";
|
|
4
|
+
import { serviceHttpFiles, serviceOperationName } from "./service-templates.mjs";
|
|
5
|
+
import { moduleApiSource } from "./ui-templates.mjs";
|
|
4
6
|
|
|
5
7
|
const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
6
8
|
const PASCAL = /^[A-Z][A-Za-z0-9]*$/;
|
|
@@ -52,31 +54,124 @@ function requirePrimary(value, label) {
|
|
|
52
54
|
return value;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
function edit(relative, update) {
|
|
58
|
+
return { relative, update };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function insertBefore(source, anchor, content, relative) {
|
|
62
|
+
const index = source.indexOf(anchor);
|
|
63
|
+
if (index < 0) fail(`Cannot update ${relative}: canonical insertion point was not found; normalize the file before generating`);
|
|
64
|
+
if (source.indexOf(anchor, index + anchor.length) >= 0) {
|
|
65
|
+
fail(`Cannot update ${relative}: canonical insertion point is ambiguous; normalize the file before generating`);
|
|
60
66
|
}
|
|
61
|
-
return
|
|
67
|
+
return `${source.slice(0, index)}${content}${source.slice(index)}`;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
function plan(files) {
|
|
70
|
+
function plan(files, edits = []) {
|
|
65
71
|
return {
|
|
66
72
|
files,
|
|
67
73
|
write(root) {
|
|
68
74
|
const existing = Object.keys(files).filter((relative) => fs.existsSync(path.join(root, relative)));
|
|
69
75
|
if (existing.length) fail(`Refusing to overwrite existing file(s):\n${existing.map((file) => ` ${file}`).join("\n")}`);
|
|
76
|
+
const updates = edits.map(({ relative, update }) => {
|
|
77
|
+
const absolute = path.join(root, relative);
|
|
78
|
+
if (!fs.existsSync(absolute)) fail(`Cannot update missing generated composition file: ${relative}`);
|
|
79
|
+
return { relative, absolute, content: update(fs.readFileSync(absolute, "utf8")) };
|
|
80
|
+
});
|
|
70
81
|
for (const [relative, content] of Object.entries(files)) {
|
|
71
82
|
const absolute = path.join(root, relative);
|
|
72
83
|
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
73
84
|
fs.writeFileSync(absolute, content);
|
|
74
85
|
}
|
|
75
|
-
|
|
86
|
+
for (const update of updates) fs.writeFileSync(update.absolute, update.content);
|
|
87
|
+
return [...Object.keys(files), ...updates.map(({ relative }) => relative)];
|
|
76
88
|
}
|
|
77
89
|
};
|
|
78
90
|
}
|
|
79
91
|
|
|
92
|
+
function registerUseCaseEdit(moduleName, verbNoun) {
|
|
93
|
+
const relative = "src/app/container.ts";
|
|
94
|
+
return edit(relative, (source) => {
|
|
95
|
+
if (new RegExp(`\\b${verbNoun}\\b`).test(source)) fail(`${relative} already references ${verbNoun}`);
|
|
96
|
+
let next = insertBefore(
|
|
97
|
+
source,
|
|
98
|
+
"\nconst register =",
|
|
99
|
+
`import { ${verbNoun} } from "@/modules/${moduleName}/use-cases/${verbNoun}";\n`,
|
|
100
|
+
relative
|
|
101
|
+
);
|
|
102
|
+
next = insertBefore(next, "\n\treturn container;", `\n\tregister(${verbNoun}, new ${verbNoun}());\n`, relative);
|
|
103
|
+
return next;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function mountModuleRoutesEdit(moduleName, verbNoun) {
|
|
108
|
+
const relative = "src/app/routes.ts";
|
|
109
|
+
const dependency = serviceOperationName(moduleName, verbNoun);
|
|
110
|
+
const factory = `${camel(moduleName)}Routes`;
|
|
111
|
+
return edit(relative, (source) => {
|
|
112
|
+
if (new RegExp(`\\b${factory}\\b`).test(source)) fail(`${relative} already mounts ${factory}`);
|
|
113
|
+
let next = insertBefore(
|
|
114
|
+
source,
|
|
115
|
+
"\nexport function applicationRoutes",
|
|
116
|
+
`import { ${verbNoun} } from "@/modules/${moduleName}/use-cases/${verbNoun}";\nimport { ${factory} } from "@/modules/${moduleName}/http/${moduleName}.routes";\n`,
|
|
117
|
+
relative
|
|
118
|
+
);
|
|
119
|
+
next = insertBefore(
|
|
120
|
+
next,
|
|
121
|
+
"\n\treturn router;",
|
|
122
|
+
`\n\trouter.use(\n\t\t${factory}({\n\t\t\t${dependency}: container.resolve(${verbNoun})\n\t\t})\n\t);\n`,
|
|
123
|
+
relative
|
|
124
|
+
);
|
|
125
|
+
return next;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extendModuleRoutesEdits(moduleName, verbNoun) {
|
|
130
|
+
const operation = serviceOperationName(moduleName, verbNoun);
|
|
131
|
+
const dependency = operation;
|
|
132
|
+
const schemasRelative = `src/modules/${moduleName}/http/${moduleName}.schemas.ts`;
|
|
133
|
+
const routesRelative = `src/modules/${moduleName}/http/${moduleName}.routes.ts`;
|
|
134
|
+
const appRoutesRelative = "src/app/routes.ts";
|
|
135
|
+
return [
|
|
136
|
+
edit(schemasRelative, (source) => {
|
|
137
|
+
if (new RegExp(`\\b${verbNoun}Schema\\b`).test(source)) fail(`${schemasRelative} already defines ${verbNoun}Schema`);
|
|
138
|
+
return `${source.trimEnd()}\n\nexport const ${verbNoun}Schema = Joi.object({}).unknown(false);\n`;
|
|
139
|
+
}),
|
|
140
|
+
edit(routesRelative, (source) => {
|
|
141
|
+
if (new RegExp(`\\b${verbNoun}\\b`).test(source)) fail(`${routesRelative} already references ${verbNoun}`);
|
|
142
|
+
let next = insertBefore(
|
|
143
|
+
source,
|
|
144
|
+
`import { rpcHandler } from "@/platform/http/rpcHandler";`,
|
|
145
|
+
`import type { ${verbNoun} } from "@/modules/${moduleName}/use-cases/${verbNoun}";\nimport { ${verbNoun}Schema } from "@/modules/${moduleName}/http/${moduleName}.schemas";\n`,
|
|
146
|
+
routesRelative
|
|
147
|
+
);
|
|
148
|
+
next = insertBefore(next, "\n}\n\nexport function", `\n\t${dependency}: ${verbNoun};`, routesRelative);
|
|
149
|
+
next = insertBefore(
|
|
150
|
+
next,
|
|
151
|
+
"\n\treturn router;",
|
|
152
|
+
`\n\trouter.post("/${moduleName}.${operation}", rpcHandler(${verbNoun}Schema, useCases.${dependency}));`,
|
|
153
|
+
routesRelative
|
|
154
|
+
);
|
|
155
|
+
return next;
|
|
156
|
+
}),
|
|
157
|
+
edit(appRoutesRelative, (source) => {
|
|
158
|
+
if (new RegExp(`\\b${verbNoun}\\b`).test(source)) fail(`${appRoutesRelative} already references ${verbNoun}`);
|
|
159
|
+
let next = insertBefore(
|
|
160
|
+
source,
|
|
161
|
+
`import { ${camel(moduleName)}Routes } from "@/modules/${moduleName}/http/${moduleName}.routes";`,
|
|
162
|
+
`import { ${verbNoun} } from "@/modules/${moduleName}/use-cases/${verbNoun}";\n`,
|
|
163
|
+
appRoutesRelative
|
|
164
|
+
);
|
|
165
|
+
const start = next.indexOf(`\t\t${camel(moduleName)}Routes({`);
|
|
166
|
+
if (start < 0) fail(`Cannot update ${appRoutesRelative}: ${camel(moduleName)}Routes mount was not found`);
|
|
167
|
+
const end = next.indexOf("\n\t\t})", start);
|
|
168
|
+
if (end < 0) fail(`Cannot update ${appRoutesRelative}: ${camel(moduleName)}Routes mount is not canonical`);
|
|
169
|
+
const before = next.slice(0, end).replace(/\n\t\t\t([^\n]+)$/, "\n\t\t\t$1,");
|
|
170
|
+
return `${before}\n\t\t\t${dependency}: container.resolve(${verbNoun})${next.slice(end)}`;
|
|
171
|
+
})
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
80
175
|
function useCaseFiles(moduleName, verbNoun) {
|
|
81
176
|
return {
|
|
82
177
|
[`src/modules/${moduleName}/use-cases/${verbNoun}.ts`]:
|
|
@@ -98,61 +193,27 @@ export class ${verbNoun} implements UseCase<${verbNoun}Input, ${verbNoun}Output>
|
|
|
98
193
|
|
|
99
194
|
describe("${verbNoun}", () => {
|
|
100
195
|
it("is not implemented yet", async () => {
|
|
101
|
-
await expect(new ${verbNoun}().execute({ requestId: "test"
|
|
196
|
+
await expect(new ${verbNoun}().execute({ requestId: "test" }, {})).rejects.toThrow();
|
|
102
197
|
});
|
|
103
198
|
});
|
|
104
199
|
`
|
|
105
200
|
};
|
|
106
201
|
}
|
|
107
202
|
|
|
108
|
-
function httpFiles(moduleName, verbNoun) {
|
|
109
|
-
const operation = operationName(moduleName, verbNoun);
|
|
110
|
-
const dependency = verbNoun[0].toLowerCase() + verbNoun.slice(1);
|
|
111
|
-
return {
|
|
112
|
-
[`src/modules/${moduleName}/http/${moduleName}.schemas.ts`]:
|
|
113
|
-
`import Joi from "joi";
|
|
114
|
-
|
|
115
|
-
export const ${verbNoun}Schema = Joi.object({}).unknown(false);
|
|
116
|
-
`,
|
|
117
|
-
[`src/modules/${moduleName}/http/${moduleName}.routes.ts`]:
|
|
118
|
-
`import { Router } from "express";
|
|
119
|
-
import type { ${verbNoun} } from "@/modules/${moduleName}/use-cases/${verbNoun}";
|
|
120
|
-
import { ${verbNoun}Schema } from "@/modules/${moduleName}/http/${moduleName}.schemas";
|
|
121
|
-
import { createRequestContext } from "@/platform/http/createRequestContext";
|
|
122
|
-
import { rpcResult } from "@/platform/http/rpc";
|
|
123
|
-
|
|
124
|
-
export function ${camel(moduleName)}Routes(${dependency}: ${verbNoun}) {
|
|
125
|
-
const router = Router();
|
|
126
|
-
router.post("/${moduleName}.${operation}", async (req, res, next) => {
|
|
127
|
-
try {
|
|
128
|
-
const input = await ${verbNoun}Schema.validateAsync(req.body ?? {});
|
|
129
|
-
rpcResult(res, await ${dependency}.execute(await createRequestContext(req), input));
|
|
130
|
-
} catch (error) {
|
|
131
|
-
next(error);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
return router;
|
|
135
|
-
}
|
|
136
|
-
`
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
203
|
const serviceSchematics = {
|
|
141
204
|
module(root, [name, verbNoun], options) {
|
|
142
205
|
requireModuleName(root, name);
|
|
143
206
|
requirePascal(verbNoun, "First use case", ["UseCases"]);
|
|
144
|
-
const files = { ...useCaseFiles(name, verbNoun), ...(options.has("--no-http") ? {} :
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
...(options.has("--no-http") ? [] : [`Mount ${camel(name)}Routes in src/app/routes.ts`]),
|
|
207
|
+
const files = { ...useCaseFiles(name, verbNoun), ...(options.has("--no-http") ? {} : serviceHttpFiles(name, verbNoun)) };
|
|
208
|
+
const edits = [registerUseCaseEdit(name, verbNoun), ...(options.has("--no-http") ? [] : [mountModuleRoutesEdit(name, verbNoun)])];
|
|
209
|
+
return [plan(files, edits), [
|
|
148
210
|
`Implement ${verbNoun}.execute and replace the placeholder test`
|
|
149
211
|
]];
|
|
150
212
|
},
|
|
151
213
|
"use-case"(root, [moduleName, verbNoun]) {
|
|
152
214
|
requireModuleName(root, moduleName);
|
|
153
215
|
requirePascal(verbNoun, "Use case", ["UseCases"]);
|
|
154
|
-
return [plan(useCaseFiles(moduleName, verbNoun)), [
|
|
155
|
-
`Register ${verbNoun} in src/app/container.ts`,
|
|
216
|
+
return [plan(useCaseFiles(moduleName, verbNoun), [registerUseCaseEdit(moduleName, verbNoun)]), [
|
|
156
217
|
`Expose it from an entry point (http route, worker, job, or cli) if the capability needs one`,
|
|
157
218
|
`Implement ${verbNoun}.execute and replace the placeholder test`
|
|
158
219
|
]];
|
|
@@ -162,8 +223,15 @@ const serviceSchematics = {
|
|
|
162
223
|
requirePascal(verbNoun, "Use case", ["UseCases"]);
|
|
163
224
|
if (!fs.existsSync(path.join(root, `src/modules/${moduleName}/use-cases/${verbNoun}.ts`)))
|
|
164
225
|
fail(`Use case src/modules/${moduleName}/use-cases/${verbNoun}.ts does not exist; generate it first`);
|
|
165
|
-
|
|
166
|
-
|
|
226
|
+
const schemas = path.join(root, `src/modules/${moduleName}/http/${moduleName}.schemas.ts`);
|
|
227
|
+
const routes = path.join(root, `src/modules/${moduleName}/http/${moduleName}.routes.ts`);
|
|
228
|
+
if (fs.existsSync(schemas) !== fs.existsSync(routes)) {
|
|
229
|
+
fail(`Module ${moduleName} has only one canonical HTTP file; restore both ${moduleName}.schemas.ts and ${moduleName}.routes.ts before generating`);
|
|
230
|
+
}
|
|
231
|
+
const firstHttp = !fs.existsSync(schemas);
|
|
232
|
+
return [plan(firstHttp ? serviceHttpFiles(moduleName, verbNoun) : {}, firstHttp
|
|
233
|
+
? [mountModuleRoutesEdit(moduleName, verbNoun)]
|
|
234
|
+
: extendModuleRoutesEdits(moduleName, verbNoun)), [
|
|
167
235
|
`Define the real request schema in ${moduleName}.schemas.ts`
|
|
168
236
|
]];
|
|
169
237
|
},
|
|
@@ -247,14 +315,7 @@ ${body}
|
|
|
247
315
|
function uiArtifacts(moduleName, kindOptions) {
|
|
248
316
|
const files = {};
|
|
249
317
|
if (kindOptions.api) {
|
|
250
|
-
files[`src/modules/${moduleName}/api/${moduleName}.api.ts`] =
|
|
251
|
-
`import { http } from "@/platform/http";
|
|
252
|
-
import type { RpcResponse } from "@/core/rpc";
|
|
253
|
-
|
|
254
|
-
export async function get${pascal(moduleName)}() {
|
|
255
|
-
return (await http.post<RpcResponse<unknown>>("${moduleName}.get")).data.result;
|
|
256
|
-
}
|
|
257
|
-
`;
|
|
318
|
+
files[`src/modules/${moduleName}/api/${moduleName}.api.ts`] = moduleApiSource(moduleName, pascal(moduleName));
|
|
258
319
|
}
|
|
259
320
|
if (kindOptions.view) {
|
|
260
321
|
const viewName = kindOptions.view.endsWith("View") ? kindOptions.view : `${kindOptions.view}View`;
|