@solid-stack/mason 1.0.11
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/LICENSE +18 -0
- package/README.md +238 -0
- package/dist/bin.cjs +3480 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +3451 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/domain.d.ts +2 -0
- package/dist/commands/domain.d.ts.map +1 -0
- package/dist/commands/feature.d.ts +2 -0
- package/dist/commands/feature.d.ts.map +1 -0
- package/dist/commands/httpHandler.d.ts +2 -0
- package/dist/commands/httpHandler.d.ts.map +1 -0
- package/dist/commands/import.d.ts +10 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/infrastructure.d.ts +2 -0
- package/dist/commands/infrastructure.d.ts.map +1 -0
- package/dist/commands/injectable.d.ts +2 -0
- package/dist/commands/injectable.d.ts.map +1 -0
- package/dist/commands/interface.d.ts +2 -0
- package/dist/commands/interface.d.ts.map +1 -0
- package/dist/commands/menu.d.ts +2 -0
- package/dist/commands/menu.d.ts.map +1 -0
- package/dist/commands/usecase.d.ts +2 -0
- package/dist/commands/usecase.d.ts.map +1 -0
- package/dist/core/domainGenerator.d.ts +4 -0
- package/dist/core/domainGenerator.d.ts.map +1 -0
- package/dist/core/featureGenerator.d.ts +3 -0
- package/dist/core/featureGenerator.d.ts.map +1 -0
- package/dist/core/httpHandlerGenerator.d.ts +8 -0
- package/dist/core/httpHandlerGenerator.d.ts.map +1 -0
- package/dist/core/infraGenerator.d.ts +60 -0
- package/dist/core/infraGenerator.d.ts.map +1 -0
- package/dist/core/injectableGenerator.d.ts +4 -0
- package/dist/core/injectableGenerator.d.ts.map +1 -0
- package/dist/core/interfaceGenerator.d.ts +4 -0
- package/dist/core/interfaceGenerator.d.ts.map +1 -0
- package/dist/core/providerManager.d.ts +16 -0
- package/dist/core/providerManager.d.ts.map +1 -0
- package/dist/core/useCaseGenerator.d.ts +5 -0
- package/dist/core/useCaseGenerator.d.ts.map +1 -0
- package/dist/index.cjs +3545 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3444 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/domain/class.hbs +17 -0
- package/dist/templates/domain/interface.hbs +5 -0
- package/dist/templates/domain/valueObject.hbs +15 -0
- package/dist/templates/domain.template.d.ts +4 -0
- package/dist/templates/domain.template.d.ts.map +1 -0
- package/dist/templates/engine.d.ts +20 -0
- package/dist/templates/engine.d.ts.map +1 -0
- package/dist/templates/express/index.hbs +1 -0
- package/dist/templates/expressIndex.template.d.ts +2 -0
- package/dist/templates/expressIndex.template.d.ts.map +1 -0
- package/dist/templates/httpHandler/handler.hbs +71 -0
- package/dist/templates/httpHandler.template.d.ts +2 -0
- package/dist/templates/httpHandler.template.d.ts.map +1 -0
- package/dist/templates/index.d.ts +10 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/infra/concrete.hbs +29 -0
- package/dist/templates/infra/stub.hbs +103 -0
- package/dist/templates/infra.template.d.ts +3 -0
- package/dist/templates/infra.template.d.ts.map +1 -0
- package/dist/templates/injectable/injectable.hbs +32 -0
- package/dist/templates/injectable/interface.hbs +3 -0
- package/dist/templates/injectable.template.d.ts +3 -0
- package/dist/templates/injectable.template.d.ts.map +1 -0
- package/dist/templates/interface/interface.hbs +19 -0
- package/dist/templates/interface.template.d.ts +2 -0
- package/dist/templates/interface.template.d.ts.map +1 -0
- package/dist/templates/provider/provider.hbs +13 -0
- package/dist/templates/provider.template.d.ts +2 -0
- package/dist/templates/provider.template.d.ts.map +1 -0
- package/dist/templates/useCase/useCase.hbs +57 -0
- package/dist/templates/useCase/useCaseTest.hbs +24 -0
- package/dist/templates/useCase.template.d.ts +3 -0
- package/dist/templates/useCase.template.d.ts.map +1 -0
- package/dist/types/index.d.ts +129 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/fileOperations.d.ts +8 -0
- package/dist/utils/fileOperations.d.ts.map +1 -0
- package/dist/utils/formatting.d.ts +43 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/project.d.ts +37 -0
- package/dist/utils/project.d.ts.map +1 -0
- package/dist/utils/prompts.d.ts +14 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/registry.d.ts +61 -0
- package/dist/utils/registry.d.ts.map +1 -0
- package/package.json +84 -0
- package/templates/domain/class.hbs +17 -0
- package/templates/domain/interface.hbs +5 -0
- package/templates/domain/valueObject.hbs +15 -0
- package/templates/express/index.hbs +1 -0
- package/templates/httpHandler/handler.hbs +71 -0
- package/templates/infra/concrete.hbs +29 -0
- package/templates/infra/stub.hbs +103 -0
- package/templates/injectable/injectable.hbs +32 -0
- package/templates/injectable/interface.hbs +3 -0
- package/templates/interface/interface.hbs +19 -0
- package/templates/provider/provider.hbs +13 -0
- package/templates/useCase/useCase.hbs +57 -0
- package/templates/useCase/useCaseTest.hbs +24 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3545 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs3 = require('fs');
|
|
4
|
+
var path7 = require('path');
|
|
5
|
+
var p4 = require('@clack/prompts');
|
|
6
|
+
var pc13 = require('picocolors');
|
|
7
|
+
var url = require('url');
|
|
8
|
+
var child_process = require('child_process');
|
|
9
|
+
var Handlebars = require('handlebars');
|
|
10
|
+
var cac = require('cac');
|
|
11
|
+
|
|
12
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
13
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
|
+
|
|
15
|
+
function _interopNamespace(e) {
|
|
16
|
+
if (e && e.__esModule) return e;
|
|
17
|
+
var n = Object.create(null);
|
|
18
|
+
if (e) {
|
|
19
|
+
Object.keys(e).forEach(function (k) {
|
|
20
|
+
if (k !== 'default') {
|
|
21
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
22
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
get: function () { return e[k]; }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
n.default = e;
|
|
30
|
+
return Object.freeze(n);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var fs3__default = /*#__PURE__*/_interopDefault(fs3);
|
|
34
|
+
var path7__default = /*#__PURE__*/_interopDefault(path7);
|
|
35
|
+
var p4__namespace = /*#__PURE__*/_interopNamespace(p4);
|
|
36
|
+
var pc13__default = /*#__PURE__*/_interopDefault(pc13);
|
|
37
|
+
var Handlebars__default = /*#__PURE__*/_interopDefault(Handlebars);
|
|
38
|
+
|
|
39
|
+
// src/utils/formatting.ts
|
|
40
|
+
function toPascalCase(str) {
|
|
41
|
+
if (!str) return "";
|
|
42
|
+
return str.replace(/[^\w\s-]/g, "").replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[a-z]/, (c) => c.toUpperCase());
|
|
43
|
+
}
|
|
44
|
+
function toCamelCase(str) {
|
|
45
|
+
if (!str) return "";
|
|
46
|
+
let s = str;
|
|
47
|
+
if (/^I[A-Z]/.test(s)) {
|
|
48
|
+
s = s.slice(1);
|
|
49
|
+
}
|
|
50
|
+
const pascal = toPascalCase(s);
|
|
51
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
52
|
+
}
|
|
53
|
+
function toKebabCase(str) {
|
|
54
|
+
if (!str) return "";
|
|
55
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[-_\s]+/g, "-").toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
function toSingular(str) {
|
|
58
|
+
if (!str) return "";
|
|
59
|
+
const lower = str.toLowerCase();
|
|
60
|
+
if (lower.endsWith("ies")) {
|
|
61
|
+
return str.slice(0, -3) + (str.endsWith("IES") ? "Y" : "y");
|
|
62
|
+
}
|
|
63
|
+
if (lower.endsWith("ses") && !lower.endsWith("sses")) {
|
|
64
|
+
return str.slice(0, -2);
|
|
65
|
+
}
|
|
66
|
+
if (lower.endsWith("s") && !lower.endsWith("ss")) {
|
|
67
|
+
return str.slice(0, -1);
|
|
68
|
+
}
|
|
69
|
+
return str;
|
|
70
|
+
}
|
|
71
|
+
function toPlural(str) {
|
|
72
|
+
if (!str) return "";
|
|
73
|
+
const lower = str.toLowerCase();
|
|
74
|
+
if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) {
|
|
75
|
+
return str.slice(0, -1) + (str.endsWith("Y") ? "IES" : "ies");
|
|
76
|
+
}
|
|
77
|
+
if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z") || lower.endsWith("ch") || lower.endsWith("sh")) {
|
|
78
|
+
return str + (str === str.toUpperCase() ? "ES" : "es");
|
|
79
|
+
}
|
|
80
|
+
return str + (str === str.toUpperCase() ? "S" : "s");
|
|
81
|
+
}
|
|
82
|
+
function ensureInterfacePrefix(name) {
|
|
83
|
+
const pascal = toPascalCase(name);
|
|
84
|
+
if (/^I[A-Z]/.test(pascal)) {
|
|
85
|
+
return pascal;
|
|
86
|
+
}
|
|
87
|
+
return `I${pascal}`;
|
|
88
|
+
}
|
|
89
|
+
function removeInterfacePrefix(name) {
|
|
90
|
+
if (/^I[A-Z]/.test(name)) {
|
|
91
|
+
return name.slice(1);
|
|
92
|
+
}
|
|
93
|
+
return name;
|
|
94
|
+
}
|
|
95
|
+
function toStubName(ifaceName) {
|
|
96
|
+
const base = removeInterfacePrefix(ifaceName);
|
|
97
|
+
if (base.startsWith("Stub")) {
|
|
98
|
+
return base;
|
|
99
|
+
}
|
|
100
|
+
return `Stub${base}`;
|
|
101
|
+
}
|
|
102
|
+
function isValidIdentifier(str) {
|
|
103
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(str);
|
|
104
|
+
}
|
|
105
|
+
function writeFileSafely(absolutePath, content, options = {}) {
|
|
106
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
107
|
+
const relPath = path7__default.default.relative(projectRoot, absolutePath).replace(/\\/g, "/");
|
|
108
|
+
const existedBefore = fs3__default.default.existsSync(absolutePath);
|
|
109
|
+
if (options.dryRun) {
|
|
110
|
+
return {
|
|
111
|
+
relativePath: relPath,
|
|
112
|
+
absolutePath,
|
|
113
|
+
content,
|
|
114
|
+
action: "preview",
|
|
115
|
+
existedBefore
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (existedBefore && !options.overwrite) {
|
|
119
|
+
return {
|
|
120
|
+
relativePath: relPath,
|
|
121
|
+
absolutePath,
|
|
122
|
+
content,
|
|
123
|
+
action: "skipped",
|
|
124
|
+
existedBefore: true
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const dir = path7__default.default.dirname(absolutePath);
|
|
128
|
+
if (!fs3__default.default.existsSync(dir)) {
|
|
129
|
+
fs3__default.default.mkdirSync(dir, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
fs3__default.default.writeFileSync(absolutePath, content, "utf-8");
|
|
132
|
+
return {
|
|
133
|
+
relativePath: relPath,
|
|
134
|
+
absolutePath,
|
|
135
|
+
content,
|
|
136
|
+
action: existedBefore ? "overwritten" : "created",
|
|
137
|
+
existedBefore
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function printGeneratedFiles(files, isDryRun = false) {
|
|
141
|
+
console.log("");
|
|
142
|
+
if (isDryRun) {
|
|
143
|
+
p4__namespace.log.warn(pc13__default.default.yellow(pc13__default.default.bold("[DRY RUN PREVIEW] No files were written to disk.")));
|
|
144
|
+
}
|
|
145
|
+
for (const f of files) {
|
|
146
|
+
if (f.action === "created") {
|
|
147
|
+
p4__namespace.log.success(`${pc13__default.default.bold(f.relativePath)} ${pc13__default.default.dim("(created)")}`);
|
|
148
|
+
} else if (f.action === "overwritten") {
|
|
149
|
+
p4__namespace.log.warn(`${pc13__default.default.bold(f.relativePath)} ${pc13__default.default.dim("(overwritten)")}`);
|
|
150
|
+
} else if (f.action === "skipped") {
|
|
151
|
+
p4__namespace.log.info(`${pc13__default.default.dim(f.relativePath)} ${pc13__default.default.dim("(skipped - already exists, pass -o to overwrite)")}`);
|
|
152
|
+
} else {
|
|
153
|
+
p4__namespace.log.info(`${pc13__default.default.bold(f.relativePath)} ${pc13__default.default.dim("(preview)")}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
158
|
+
let current = path7__default.default.resolve(startDir);
|
|
159
|
+
while (true) {
|
|
160
|
+
if (fs3__default.default.existsSync(path7__default.default.join(current, "package.json"))) {
|
|
161
|
+
return current;
|
|
162
|
+
}
|
|
163
|
+
const parent = path7__default.default.dirname(current);
|
|
164
|
+
if (parent === current) {
|
|
165
|
+
return path7__default.default.resolve(startDir);
|
|
166
|
+
}
|
|
167
|
+
current = parent;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function resolveFeaturesDir(projectRoot) {
|
|
171
|
+
return path7__default.default.join(projectRoot, "features");
|
|
172
|
+
}
|
|
173
|
+
function listExistingFeatures(projectRoot) {
|
|
174
|
+
const featuresDir = resolveFeaturesDir(projectRoot);
|
|
175
|
+
if (!fs3__default.default.existsSync(featuresDir)) return [];
|
|
176
|
+
try {
|
|
177
|
+
const entries = fs3__default.default.readdirSync(featuresDir, { withFileTypes: true });
|
|
178
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
179
|
+
} catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function listDomainInterfaces(projectRoot, featureName) {
|
|
184
|
+
const results = [];
|
|
185
|
+
const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
|
|
186
|
+
for (const feat of features) {
|
|
187
|
+
const domainDir = path7__default.default.join(resolveFeaturesDir(projectRoot), feat, "domain");
|
|
188
|
+
if (!fs3__default.default.existsSync(domainDir)) continue;
|
|
189
|
+
try {
|
|
190
|
+
const files = fs3__default.default.readdirSync(domainDir);
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
|
|
193
|
+
const name = file.replace(/\.ts$/, "");
|
|
194
|
+
const fullPath = path7__default.default.join(domainDir, file);
|
|
195
|
+
if (/^I[A-Z]/.test(name)) {
|
|
196
|
+
results.push({
|
|
197
|
+
name,
|
|
198
|
+
fullPath,
|
|
199
|
+
feature: feat
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
try {
|
|
203
|
+
const content = fs3__default.default.readFileSync(fullPath, "utf-8");
|
|
204
|
+
if (/export\s+(?:abstract\s+class|interface)\s+(\w+)/.test(content)) {
|
|
205
|
+
results.push({
|
|
206
|
+
name,
|
|
207
|
+
fullPath,
|
|
208
|
+
feature: feat
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return results;
|
|
220
|
+
}
|
|
221
|
+
function listDomainEntities(projectRoot, featureName) {
|
|
222
|
+
const results = [];
|
|
223
|
+
const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
|
|
224
|
+
for (const feat of features) {
|
|
225
|
+
const domainDir = path7__default.default.join(resolveFeaturesDir(projectRoot), feat, "domain");
|
|
226
|
+
if (!fs3__default.default.existsSync(domainDir)) continue;
|
|
227
|
+
try {
|
|
228
|
+
const files = fs3__default.default.readdirSync(domainDir);
|
|
229
|
+
for (const file of files) {
|
|
230
|
+
if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
|
|
231
|
+
const name = file.replace(/\.ts$/, "");
|
|
232
|
+
if (!/^I[A-Z]/.test(name)) {
|
|
233
|
+
results.push({
|
|
234
|
+
name,
|
|
235
|
+
fullPath: path7__default.default.join(domainDir, file),
|
|
236
|
+
feature: feat
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
246
|
+
function listUseCases(projectRoot, featureName) {
|
|
247
|
+
const results = [];
|
|
248
|
+
const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
|
|
249
|
+
for (const feat of features) {
|
|
250
|
+
const useCasesDir = path7__default.default.join(resolveFeaturesDir(projectRoot), feat, "useCases");
|
|
251
|
+
if (!fs3__default.default.existsSync(useCasesDir)) continue;
|
|
252
|
+
try {
|
|
253
|
+
const files = fs3__default.default.readdirSync(useCasesDir);
|
|
254
|
+
for (const file of files) {
|
|
255
|
+
if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
|
|
256
|
+
const name = file.replace(/\.ts$/, "");
|
|
257
|
+
results.push({
|
|
258
|
+
name,
|
|
259
|
+
fullPath: path7__default.default.join(useCasesDir, file),
|
|
260
|
+
feature: feat
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return results;
|
|
268
|
+
}
|
|
269
|
+
var PromptCancelledError = class extends Error {
|
|
270
|
+
constructor(message = "Operation cancelled.") {
|
|
271
|
+
super(message);
|
|
272
|
+
this.name = "PromptCancelledError";
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
function handleCancel(result) {
|
|
276
|
+
if (p4__namespace.isCancel(result)) {
|
|
277
|
+
p4__namespace.cancel("Operation cancelled.");
|
|
278
|
+
throw new PromptCancelledError();
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
function printBanner() {
|
|
283
|
+
console.log("");
|
|
284
|
+
p4__namespace.intro(pc13__default.default.bgCyan(pc13__default.default.black(" @solid-stack/mason ")));
|
|
285
|
+
}
|
|
286
|
+
async function promptProperties(options = {}) {
|
|
287
|
+
const properties = options.defaults ? [...options.defaults] : [];
|
|
288
|
+
if (properties.length > 0) {
|
|
289
|
+
const summary = properties.map((p15) => `${p15.name}: ${p15.type}${p15.optional ? "?" : ""}`).join(", ");
|
|
290
|
+
p4__namespace.log.info(pc13__default.default.dim(`Pre-configured fields: ${summary}`));
|
|
291
|
+
const addMore = handleCancel(
|
|
292
|
+
await p4__namespace.confirm({
|
|
293
|
+
message: "Would you like to add custom fields/properties?",
|
|
294
|
+
initialValue: false
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
if (!addMore) {
|
|
298
|
+
return properties;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
p4__namespace.log.info(pc13__default.default.cyan("Enter properties (leave name blank when finished):"));
|
|
302
|
+
while (true) {
|
|
303
|
+
const name = handleCancel(
|
|
304
|
+
await p4__namespace.text({
|
|
305
|
+
message: "Property name (or press Enter to finish):",
|
|
306
|
+
placeholder: "e.g. title, email, status"
|
|
307
|
+
})
|
|
308
|
+
);
|
|
309
|
+
if (!name || !name.trim()) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
const type = handleCancel(
|
|
313
|
+
await p4__namespace.select({
|
|
314
|
+
message: `Type for property "${name.trim()}":`,
|
|
315
|
+
options: [
|
|
316
|
+
{ value: "string", label: "string" },
|
|
317
|
+
{ value: "number", label: "number" },
|
|
318
|
+
{ value: "boolean", label: "boolean" },
|
|
319
|
+
{ value: "Date", label: "Date" },
|
|
320
|
+
{ value: "string[]", label: "string[]" },
|
|
321
|
+
{ value: "Record<string, unknown>", label: "Record<string, unknown>" },
|
|
322
|
+
{ value: "custom", label: "Custom type..." }
|
|
323
|
+
],
|
|
324
|
+
initialValue: "string"
|
|
325
|
+
})
|
|
326
|
+
);
|
|
327
|
+
let finalType = type;
|
|
328
|
+
if (type === "custom") {
|
|
329
|
+
finalType = handleCancel(
|
|
330
|
+
await p4__namespace.text({
|
|
331
|
+
message: `Enter custom type for "${name.trim()}":`,
|
|
332
|
+
placeholder: "e.g. UserRole, string | null",
|
|
333
|
+
validate: (val) => val && val.trim() ? void 0 : "Type cannot be empty"
|
|
334
|
+
})
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const optional = handleCancel(
|
|
338
|
+
await p4__namespace.confirm({
|
|
339
|
+
message: `Is "${name.trim()}" optional?`,
|
|
340
|
+
initialValue: false
|
|
341
|
+
})
|
|
342
|
+
);
|
|
343
|
+
properties.push({
|
|
344
|
+
name: name.trim(),
|
|
345
|
+
type: finalType.trim(),
|
|
346
|
+
optional
|
|
347
|
+
});
|
|
348
|
+
p4__namespace.log.success(
|
|
349
|
+
pc13__default.default.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return properties;
|
|
353
|
+
}
|
|
354
|
+
var DEFAULT_REGISTRY_BASE = "https://raw.githubusercontent.com/solid-stack-digital/mason-registry/main";
|
|
355
|
+
function getRegistryBase(projectRoot, override) {
|
|
356
|
+
if (override && override.trim()) {
|
|
357
|
+
return override.trim();
|
|
358
|
+
}
|
|
359
|
+
if (process.env.MASON_REGISTRY_URL && process.env.MASON_REGISTRY_URL.trim()) {
|
|
360
|
+
return process.env.MASON_REGISTRY_URL.trim();
|
|
361
|
+
}
|
|
362
|
+
if (projectRoot) {
|
|
363
|
+
const configPath = path7__default.default.join(projectRoot, "mason.config.json");
|
|
364
|
+
if (fs3__default.default.existsSync(configPath)) {
|
|
365
|
+
try {
|
|
366
|
+
const config = JSON.parse(fs3__default.default.readFileSync(configPath, "utf-8"));
|
|
367
|
+
if (config.registry && typeof config.registry === "string") {
|
|
368
|
+
return config.registry.trim();
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (projectRoot) {
|
|
375
|
+
const siblingRegistry = path7__default.default.resolve(projectRoot, "../mason-registry");
|
|
376
|
+
if (fs3__default.default.existsSync(siblingRegistry) && fs3__default.default.existsSync(path7__default.default.join(siblingRegistry, "index.json"))) {
|
|
377
|
+
return siblingRegistry;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return DEFAULT_REGISTRY_BASE;
|
|
381
|
+
}
|
|
382
|
+
async function fetchRegistryFile(registryBase, relPath) {
|
|
383
|
+
const cleanRelPath = relPath.replace(/^\/+/, "");
|
|
384
|
+
if (registryBase.startsWith("http://") || registryBase.startsWith("https://")) {
|
|
385
|
+
const url = `${registryBase.replace(/\/+$/, "")}/${cleanRelPath}`;
|
|
386
|
+
const res = await fetch(url);
|
|
387
|
+
if (!res.ok) {
|
|
388
|
+
throw new Error(`Failed to fetch ${cleanRelPath} from ${url} (HTTP ${res.status}: ${res.statusText})`);
|
|
389
|
+
}
|
|
390
|
+
return await res.text();
|
|
391
|
+
}
|
|
392
|
+
let baseDir = registryBase;
|
|
393
|
+
if (baseDir.startsWith("file://")) {
|
|
394
|
+
baseDir = url.fileURLToPath(baseDir);
|
|
395
|
+
}
|
|
396
|
+
const filePath = path7__default.default.resolve(baseDir, cleanRelPath);
|
|
397
|
+
if (!fs3__default.default.existsSync(filePath)) {
|
|
398
|
+
throw new Error(`Registry file not found at ${filePath}`);
|
|
399
|
+
}
|
|
400
|
+
return fs3__default.default.promises.readFile(filePath, "utf-8");
|
|
401
|
+
}
|
|
402
|
+
async function fetchRegistryIndex(registryBase) {
|
|
403
|
+
const content = await fetchRegistryFile(registryBase, "index.json");
|
|
404
|
+
return JSON.parse(content);
|
|
405
|
+
}
|
|
406
|
+
async function fetchModuleManifest(registryBase, type, name) {
|
|
407
|
+
const folder = type === "shared" ? "shared" : "features";
|
|
408
|
+
const manifestPath = `${folder}/${name}/registry.json`;
|
|
409
|
+
const content = await fetchRegistryFile(registryBase, manifestPath);
|
|
410
|
+
return JSON.parse(content);
|
|
411
|
+
}
|
|
412
|
+
function levenshteinDistance(a, b) {
|
|
413
|
+
const m = a.length;
|
|
414
|
+
const n = b.length;
|
|
415
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
416
|
+
for (let i = 0; i <= m; i++) {
|
|
417
|
+
dp[i][0] = i;
|
|
418
|
+
}
|
|
419
|
+
for (let j = 0; j <= n; j++) {
|
|
420
|
+
dp[0][j] = j;
|
|
421
|
+
}
|
|
422
|
+
for (let i = 1; i <= m; i++) {
|
|
423
|
+
for (let j = 1; j <= n; j++) {
|
|
424
|
+
if (a[i - 1] === b[j - 1]) {
|
|
425
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
426
|
+
} else {
|
|
427
|
+
dp[i][j] = Math.min(
|
|
428
|
+
dp[i - 1][j] + 1,
|
|
429
|
+
// deletion
|
|
430
|
+
dp[i][j - 1] + 1,
|
|
431
|
+
// insertion
|
|
432
|
+
dp[i - 1][j - 1] + 1
|
|
433
|
+
// substitution
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return dp[m][n];
|
|
439
|
+
}
|
|
440
|
+
function findClosestMatch(target, candidates) {
|
|
441
|
+
if (candidates.length === 0) return null;
|
|
442
|
+
const lower = target.toLowerCase();
|
|
443
|
+
let closest = null;
|
|
444
|
+
let minDistance = Infinity;
|
|
445
|
+
for (const candidate of candidates) {
|
|
446
|
+
const candLower = candidate.toLowerCase();
|
|
447
|
+
if (candLower.includes(lower) || lower.includes(candLower)) {
|
|
448
|
+
return candidate;
|
|
449
|
+
}
|
|
450
|
+
const dist = levenshteinDistance(lower, candLower);
|
|
451
|
+
if (dist < minDistance) {
|
|
452
|
+
minDistance = dist;
|
|
453
|
+
closest = candidate;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (minDistance <= 3) {
|
|
457
|
+
return closest;
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
function detectPackageManager(projectRoot) {
|
|
462
|
+
if (fs3__default.default.existsSync(path7__default.default.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
463
|
+
if (fs3__default.default.existsSync(path7__default.default.join(projectRoot, "yarn.lock"))) return "yarn";
|
|
464
|
+
if (fs3__default.default.existsSync(path7__default.default.join(projectRoot, "bun.lockb")) || fs3__default.default.existsSync(path7__default.default.join(projectRoot, "bun.lock"))) return "bun";
|
|
465
|
+
return "npm";
|
|
466
|
+
}
|
|
467
|
+
function getMissingNpmPackages(projectRoot, requiredPackages) {
|
|
468
|
+
if (!requiredPackages || requiredPackages.length === 0) return [];
|
|
469
|
+
const pkgJsonPath = path7__default.default.join(projectRoot, "package.json");
|
|
470
|
+
if (!fs3__default.default.existsSync(pkgJsonPath)) {
|
|
471
|
+
return requiredPackages;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const pkg = JSON.parse(fs3__default.default.readFileSync(pkgJsonPath, "utf-8"));
|
|
475
|
+
const installed = /* @__PURE__ */ new Set([
|
|
476
|
+
...Object.keys(pkg.dependencies || {}),
|
|
477
|
+
...Object.keys(pkg.devDependencies || {}),
|
|
478
|
+
...Object.keys(pkg.peerDependencies || {})
|
|
479
|
+
]);
|
|
480
|
+
return requiredPackages.filter((p15) => {
|
|
481
|
+
const nameOnly = p15.startsWith("@") ? `@${p15.slice(1).split("@")[0] || ""}` : p15.split("@")[0] || p15;
|
|
482
|
+
return !installed.has(nameOnly);
|
|
483
|
+
});
|
|
484
|
+
} catch {
|
|
485
|
+
return requiredPackages;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function installNpmPackages(projectRoot, packages, pm) {
|
|
489
|
+
if (!packages || packages.length === 0) return;
|
|
490
|
+
const manager = pm || detectPackageManager(projectRoot);
|
|
491
|
+
let installCmd;
|
|
492
|
+
switch (manager) {
|
|
493
|
+
case "pnpm":
|
|
494
|
+
installCmd = `pnpm add ${packages.join(" ")}`;
|
|
495
|
+
break;
|
|
496
|
+
case "yarn":
|
|
497
|
+
installCmd = `yarn add ${packages.join(" ")}`;
|
|
498
|
+
break;
|
|
499
|
+
case "bun":
|
|
500
|
+
installCmd = `bun add ${packages.join(" ")}`;
|
|
501
|
+
break;
|
|
502
|
+
default:
|
|
503
|
+
installCmd = `npm install ${packages.join(" ")}`;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
p4__namespace.log.info(`Installing missing dependencies with ${manager}: ${pc13__default.default.cyan(packages.join(", "))}`);
|
|
507
|
+
child_process.execSync(installCmd, {
|
|
508
|
+
cwd: projectRoot,
|
|
509
|
+
stdio: "inherit"
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
var hbs = Handlebars__default.default.create();
|
|
513
|
+
hbs.registerHelper("eq", (a, b) => a === b);
|
|
514
|
+
hbs.registerHelper("ne", (a, b) => a !== b);
|
|
515
|
+
hbs.registerHelper("not", (a) => !a);
|
|
516
|
+
hbs.registerHelper("and", (...args) => args.slice(0, -1).every(Boolean));
|
|
517
|
+
hbs.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean));
|
|
518
|
+
hbs.registerHelper("pascalCase", (str) => toPascalCase(str));
|
|
519
|
+
hbs.registerHelper("camelCase", (str) => toCamelCase(str));
|
|
520
|
+
hbs.registerHelper("kebabCase", (str) => toKebabCase(str));
|
|
521
|
+
hbs.registerHelper("join", (arr, sep) => {
|
|
522
|
+
if (!Array.isArray(arr)) return "";
|
|
523
|
+
const delimiter = typeof sep === "string" ? sep : ", ";
|
|
524
|
+
return arr.join(delimiter);
|
|
525
|
+
});
|
|
526
|
+
var cachedTemplatesDir = null;
|
|
527
|
+
function getTemplatesDir() {
|
|
528
|
+
if (cachedTemplatesDir && fs3__default.default.existsSync(cachedTemplatesDir)) {
|
|
529
|
+
return cachedTemplatesDir;
|
|
530
|
+
}
|
|
531
|
+
const currentDir = typeof __dirname !== "undefined" ? __dirname : path7__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
532
|
+
const cand1 = path7__default.default.resolve(currentDir, "../../templates");
|
|
533
|
+
if (fs3__default.default.existsSync(cand1)) {
|
|
534
|
+
cachedTemplatesDir = cand1;
|
|
535
|
+
return cand1;
|
|
536
|
+
}
|
|
537
|
+
const cand2 = path7__default.default.resolve(currentDir, "../templates");
|
|
538
|
+
if (fs3__default.default.existsSync(cand2)) {
|
|
539
|
+
cachedTemplatesDir = cand2;
|
|
540
|
+
return cand2;
|
|
541
|
+
}
|
|
542
|
+
const cand3 = path7__default.default.resolve(currentDir, "templates");
|
|
543
|
+
if (fs3__default.default.existsSync(cand3)) {
|
|
544
|
+
cachedTemplatesDir = cand3;
|
|
545
|
+
return cand3;
|
|
546
|
+
}
|
|
547
|
+
let curr = currentDir;
|
|
548
|
+
while (true) {
|
|
549
|
+
const candTemplates = path7__default.default.join(curr, "templates");
|
|
550
|
+
if (fs3__default.default.existsSync(candTemplates)) {
|
|
551
|
+
cachedTemplatesDir = candTemplates;
|
|
552
|
+
return candTemplates;
|
|
553
|
+
}
|
|
554
|
+
const parent = path7__default.default.dirname(curr);
|
|
555
|
+
if (parent === curr) break;
|
|
556
|
+
curr = parent;
|
|
557
|
+
}
|
|
558
|
+
throw new Error("Could not locate templates directory for @solid-stack/create");
|
|
559
|
+
}
|
|
560
|
+
var templateCache = /* @__PURE__ */ new Map();
|
|
561
|
+
function loadTemplate(relativePath) {
|
|
562
|
+
const fullPath = path7__default.default.isAbsolute(relativePath) ? relativePath : path7__default.default.join(getTemplatesDir(), relativePath);
|
|
563
|
+
return fs3__default.default.readFileSync(fullPath, "utf-8");
|
|
564
|
+
}
|
|
565
|
+
function renderTemplateFile(relativePath, context) {
|
|
566
|
+
let compiled = templateCache.get(relativePath);
|
|
567
|
+
if (!compiled) {
|
|
568
|
+
const source = loadTemplate(relativePath);
|
|
569
|
+
compiled = hbs.compile(source.trim(), { noEscape: true });
|
|
570
|
+
templateCache.set(relativePath, compiled);
|
|
571
|
+
}
|
|
572
|
+
const result = compiled(context);
|
|
573
|
+
return result.endsWith("\n") ? result : result + "\n";
|
|
574
|
+
}
|
|
575
|
+
function renderTemplate(sourceOrPath, context) {
|
|
576
|
+
if (sourceOrPath.endsWith(".hbs")) {
|
|
577
|
+
return renderTemplateFile(sourceOrPath, context);
|
|
578
|
+
}
|
|
579
|
+
let compiled = templateCache.get(sourceOrPath);
|
|
580
|
+
if (!compiled) {
|
|
581
|
+
compiled = hbs.compile(sourceOrPath.trim(), { noEscape: true });
|
|
582
|
+
templateCache.set(sourceOrPath, compiled);
|
|
583
|
+
}
|
|
584
|
+
const result = compiled(context);
|
|
585
|
+
return result.endsWith("\n") ? result : result + "\n";
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/templates/domain.template.ts
|
|
589
|
+
var domainInterfaceTemplate = loadTemplate("domain/interface.hbs");
|
|
590
|
+
var domainClassTemplate = loadTemplate("domain/class.hbs");
|
|
591
|
+
var domainValueObjectTemplate = loadTemplate("domain/valueObject.hbs");
|
|
592
|
+
|
|
593
|
+
// src/templates/interface.template.ts
|
|
594
|
+
var interfaceTemplate = loadTemplate("interface/interface.hbs");
|
|
595
|
+
|
|
596
|
+
// src/templates/useCase.template.ts
|
|
597
|
+
var useCaseTemplate = loadTemplate("useCase/useCase.hbs");
|
|
598
|
+
var useCaseTestTemplate = loadTemplate("useCase/useCaseTest.hbs");
|
|
599
|
+
|
|
600
|
+
// src/templates/infra.template.ts
|
|
601
|
+
var concreteInfraTemplate = loadTemplate("infra/concrete.hbs");
|
|
602
|
+
var stubInfraTemplate = loadTemplate("infra/stub.hbs");
|
|
603
|
+
|
|
604
|
+
// src/templates/httpHandler.template.ts
|
|
605
|
+
var httpHandlerTemplate = loadTemplate("httpHandler/handler.hbs");
|
|
606
|
+
|
|
607
|
+
// src/templates/injectable.template.ts
|
|
608
|
+
var injectableTemplate = loadTemplate("injectable/injectable.hbs");
|
|
609
|
+
var injectableInterfaceTemplate = loadTemplate("injectable/interface.hbs");
|
|
610
|
+
|
|
611
|
+
// src/templates/provider.template.ts
|
|
612
|
+
var diProviderTemplate = loadTemplate("provider/provider.hbs");
|
|
613
|
+
|
|
614
|
+
// src/templates/expressIndex.template.ts
|
|
615
|
+
var expressIndexTemplate = loadTemplate("express/index.hbs");
|
|
616
|
+
function generateDomainEntityCode(name, kind = "interface", properties = []) {
|
|
617
|
+
const entityName = toPascalCase(name);
|
|
618
|
+
const defaultProps = [
|
|
619
|
+
{ name: "id", type: "string" },
|
|
620
|
+
{ name: "createdAt", type: "Date" },
|
|
621
|
+
{ name: "updatedAt", type: "Date" }
|
|
622
|
+
];
|
|
623
|
+
const effectiveProps = properties.length > 0 ? properties : defaultProps;
|
|
624
|
+
if (kind === "interface") {
|
|
625
|
+
return renderTemplate(domainInterfaceTemplate, {
|
|
626
|
+
name: entityName,
|
|
627
|
+
properties: effectiveProps
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (kind === "class") {
|
|
631
|
+
return renderTemplate(domainClassTemplate, {
|
|
632
|
+
name: entityName,
|
|
633
|
+
properties: effectiveProps
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (kind === "valueObject") {
|
|
637
|
+
const valueProp = effectiveProps[0] || { name: "value", type: "string" };
|
|
638
|
+
return renderTemplate(domainValueObjectTemplate, {
|
|
639
|
+
name: entityName,
|
|
640
|
+
valueProp
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return "";
|
|
644
|
+
}
|
|
645
|
+
function generateDomain(options) {
|
|
646
|
+
const projectRoot = options.projectRoot || findProjectRoot();
|
|
647
|
+
const entityName = toPascalCase(options.name);
|
|
648
|
+
const targetDir = options.targetDir || path7__default.default.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
|
|
649
|
+
const filePath = path7__default.default.join(targetDir, `${entityName}.ts`);
|
|
650
|
+
const code = generateDomainEntityCode(
|
|
651
|
+
entityName,
|
|
652
|
+
options.kind || "interface",
|
|
653
|
+
options.properties || []
|
|
654
|
+
);
|
|
655
|
+
return writeFileSafely(filePath, code, {
|
|
656
|
+
projectRoot,
|
|
657
|
+
dryRun: options.dryRun,
|
|
658
|
+
overwrite: options.overwrite
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
function generateInterfaceCode(ifaceName, kind = "abstractClass", entityName, methodsPreset = "crud", customMethods = []) {
|
|
662
|
+
const validIfaceName = ensureInterfacePrefix(ifaceName);
|
|
663
|
+
const baseEntity = entityName ? toPascalCase(entityName) : toPascalCase(removeInterfacePrefix(validIfaceName).replace(/Repository$|Service$|Client$/, ""));
|
|
664
|
+
const isCrud = methodsPreset === "crud" && Boolean(baseEntity);
|
|
665
|
+
const isCustom = methodsPreset === "custom" && customMethods.length > 0;
|
|
666
|
+
const isAbstractClass = kind === "abstractClass";
|
|
667
|
+
const methodPrefix = isAbstractClass ? "abstract " : "";
|
|
668
|
+
const domainImport = isCrud ? baseEntity : void 0;
|
|
669
|
+
const entityKey = baseEntity ? baseEntity.charAt(0).toLowerCase() + baseEntity.slice(1) : "";
|
|
670
|
+
const idKey = `${entityKey}Id`;
|
|
671
|
+
const listKey = `${entityKey}s`;
|
|
672
|
+
const formattedCustomMethods = customMethods.map((m) => {
|
|
673
|
+
const params = m.params && m.params.length > 0 ? m.params.map((p15) => `${p15.name}: ${p15.type}`).join("; ") : "";
|
|
674
|
+
const returnType = m.returnType ? `Promise<${m.returnType}>` : "Promise<void>";
|
|
675
|
+
return {
|
|
676
|
+
name: m.name,
|
|
677
|
+
params,
|
|
678
|
+
returnType
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
return renderTemplate(interfaceTemplate, {
|
|
682
|
+
ifaceName: validIfaceName,
|
|
683
|
+
isAbstractClass,
|
|
684
|
+
domainImport,
|
|
685
|
+
isCrud,
|
|
686
|
+
baseEntity,
|
|
687
|
+
idKey,
|
|
688
|
+
entityKey,
|
|
689
|
+
listKey,
|
|
690
|
+
methodPrefix,
|
|
691
|
+
isCustom,
|
|
692
|
+
customMethods: formattedCustomMethods
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function generateInterface(options) {
|
|
696
|
+
const projectRoot = options.projectRoot || findProjectRoot();
|
|
697
|
+
const ifaceName = ensureInterfacePrefix(options.name);
|
|
698
|
+
const targetDir = options.targetDir || path7__default.default.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
|
|
699
|
+
const filePath = path7__default.default.join(targetDir, `${ifaceName}.ts`);
|
|
700
|
+
const code = generateInterfaceCode(
|
|
701
|
+
ifaceName,
|
|
702
|
+
options.kind || "abstractClass",
|
|
703
|
+
options.entityName,
|
|
704
|
+
options.methodsPreset || "crud",
|
|
705
|
+
options.customMethods || []
|
|
706
|
+
);
|
|
707
|
+
return writeFileSafely(filePath, code, {
|
|
708
|
+
projectRoot,
|
|
709
|
+
dryRun: options.dryRun,
|
|
710
|
+
overwrite: options.overwrite
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
function parseInterfaceMethodsAndImports(content, targetIfaceName) {
|
|
714
|
+
const imports = [];
|
|
715
|
+
const methods = [];
|
|
716
|
+
let extendsClass;
|
|
717
|
+
const importRegex = /import\s+(type\s+)?(?:(\w+)|\{([^}]+)\}|\*\s+as\s+(\w+))\s+from\s+["']([^"']+)["'];?/g;
|
|
718
|
+
let impMatch;
|
|
719
|
+
while ((impMatch = importRegex.exec(content)) !== null) {
|
|
720
|
+
const isGlobalType = Boolean(impMatch[1]);
|
|
721
|
+
const defaultImport = impMatch[2];
|
|
722
|
+
const namedImports = impMatch[3];
|
|
723
|
+
const namespaceImport = impMatch[4];
|
|
724
|
+
const specifier = impMatch[5];
|
|
725
|
+
const items = [];
|
|
726
|
+
if (defaultImport) {
|
|
727
|
+
items.push({ name: defaultImport, isTypeOnly: isGlobalType ? true : void 0 });
|
|
728
|
+
} else if (namespaceImport) {
|
|
729
|
+
items.push({ name: namespaceImport, isTypeOnly: isGlobalType ? true : void 0 });
|
|
730
|
+
} else if (namedImports) {
|
|
731
|
+
const parts = namedImports.split(",");
|
|
732
|
+
for (const part of parts) {
|
|
733
|
+
const trimmed = part.trim();
|
|
734
|
+
if (!trimmed) continue;
|
|
735
|
+
const typeMatch = trimmed.match(/^type\s+(\w+)(?:\s+as\s+\w+)?$/);
|
|
736
|
+
const normalMatch = trimmed.match(/^(\w+)(?:\s+as\s+\w+)?$/);
|
|
737
|
+
if (typeMatch) {
|
|
738
|
+
items.push({ name: typeMatch[1], isTypeOnly: true });
|
|
739
|
+
} else if (normalMatch) {
|
|
740
|
+
items.push({ name: normalMatch[1], isTypeOnly: isGlobalType ? true : void 0 });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (items.length > 0) {
|
|
745
|
+
imports.push({
|
|
746
|
+
specifier,
|
|
747
|
+
items,
|
|
748
|
+
isTypeOnly: isGlobalType
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
let classRegex;
|
|
753
|
+
if (targetIfaceName) {
|
|
754
|
+
classRegex = new RegExp(
|
|
755
|
+
`(?:export\\s+)?(?:abstract\\s+class|interface)\\s+(${targetIfaceName})\\b([^{]*)\\{`,
|
|
756
|
+
"g"
|
|
757
|
+
);
|
|
758
|
+
} else {
|
|
759
|
+
classRegex = /(?:export\\s+)?(?:abstract\\s+class|interface)\\s+([A-Za-z0-9_$]+)\b([^{]*)\{/g;
|
|
760
|
+
}
|
|
761
|
+
const declMatch = classRegex.exec(content);
|
|
762
|
+
if (!declMatch) {
|
|
763
|
+
return {
|
|
764
|
+
className: targetIfaceName || "",
|
|
765
|
+
methods: [],
|
|
766
|
+
imports
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const className = declMatch[1];
|
|
770
|
+
const headerRemainder = declMatch[2] || "";
|
|
771
|
+
const headerEnd = declMatch.index + declMatch[0].length;
|
|
772
|
+
const extMatch = headerRemainder.match(/\bextends\s+([A-Za-z0-9_$]+)\b/);
|
|
773
|
+
if (extMatch) {
|
|
774
|
+
extendsClass = extMatch[1];
|
|
775
|
+
}
|
|
776
|
+
let depth = 1;
|
|
777
|
+
let bodyEnd = -1;
|
|
778
|
+
for (let i = headerEnd; i < content.length; i++) {
|
|
779
|
+
const ch = content[i];
|
|
780
|
+
if (ch === "/" && content[i + 1] === "/") {
|
|
781
|
+
const nl = content.indexOf("\n", i);
|
|
782
|
+
i = nl === -1 ? content.length : nl;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (ch === "/" && content[i + 1] === "*") {
|
|
786
|
+
const closeComment = content.indexOf("*/", i + 2);
|
|
787
|
+
i = closeComment === -1 ? content.length : closeComment + 1;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
791
|
+
const quote = ch;
|
|
792
|
+
i++;
|
|
793
|
+
while (i < content.length && content[i] !== quote) {
|
|
794
|
+
if (content[i] === "\\") i++;
|
|
795
|
+
i++;
|
|
796
|
+
}
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (ch === "{") {
|
|
800
|
+
depth++;
|
|
801
|
+
} else if (ch === "}") {
|
|
802
|
+
depth--;
|
|
803
|
+
if (depth === 0) {
|
|
804
|
+
bodyEnd = i;
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (bodyEnd === -1) {
|
|
810
|
+
bodyEnd = content.length;
|
|
811
|
+
}
|
|
812
|
+
const body = content.slice(headerEnd, bodyEnd);
|
|
813
|
+
const statements = [];
|
|
814
|
+
let currentStmt = "";
|
|
815
|
+
let pDepth = 0;
|
|
816
|
+
let bDepth = 0;
|
|
817
|
+
let sDepth = 0;
|
|
818
|
+
let aDepth = 0;
|
|
819
|
+
for (let i = 0; i < body.length; i++) {
|
|
820
|
+
const ch = body[i];
|
|
821
|
+
if (ch === "/" && body[i + 1] === "/") {
|
|
822
|
+
const nl = body.indexOf("\n", i);
|
|
823
|
+
i = nl === -1 ? body.length : nl;
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (ch === "/" && body[i + 1] === "*") {
|
|
827
|
+
const closeComment = body.indexOf("*/", i + 2);
|
|
828
|
+
i = closeComment === -1 ? body.length : closeComment + 1;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
832
|
+
const quote = ch;
|
|
833
|
+
currentStmt += ch;
|
|
834
|
+
i++;
|
|
835
|
+
while (i < body.length && body[i] !== quote) {
|
|
836
|
+
if (body[i] === "\\") {
|
|
837
|
+
currentStmt += body[i];
|
|
838
|
+
i++;
|
|
839
|
+
}
|
|
840
|
+
if (i < body.length) currentStmt += body[i];
|
|
841
|
+
i++;
|
|
842
|
+
}
|
|
843
|
+
if (i < body.length) currentStmt += body[i];
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (ch === "(") pDepth++;
|
|
847
|
+
else if (ch === ")") pDepth = Math.max(0, pDepth - 1);
|
|
848
|
+
else if (ch === "{") bDepth++;
|
|
849
|
+
else if (ch === "}") bDepth = Math.max(0, bDepth - 1);
|
|
850
|
+
else if (ch === "[") sDepth++;
|
|
851
|
+
else if (ch === "]") sDepth = Math.max(0, sDepth - 1);
|
|
852
|
+
else if (ch === "<") aDepth++;
|
|
853
|
+
else if (ch === ">") aDepth = Math.max(0, aDepth - 1);
|
|
854
|
+
if (ch === ";" && pDepth === 0 && bDepth === 0 && sDepth === 0 && aDepth === 0) {
|
|
855
|
+
if (currentStmt.trim()) {
|
|
856
|
+
statements.push(currentStmt.trim());
|
|
857
|
+
}
|
|
858
|
+
currentStmt = "";
|
|
859
|
+
} else {
|
|
860
|
+
currentStmt += ch;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (currentStmt.trim()) {
|
|
864
|
+
statements.push(currentStmt.trim());
|
|
865
|
+
}
|
|
866
|
+
for (let stmt of statements) {
|
|
867
|
+
stmt = stmt.replace(/\/\*[\s\S]*?\*\//g, "").trim();
|
|
868
|
+
stmt = stmt.replace(/^(?:public\s+|protected\s+|private\s+)?(?:abstract\s+)?(?:readonly\s+)?/, "").trim();
|
|
869
|
+
const firstParen = stmt.indexOf("(");
|
|
870
|
+
if (firstParen === -1) continue;
|
|
871
|
+
let methodName = "";
|
|
872
|
+
let typeParams;
|
|
873
|
+
const firstAngle = stmt.indexOf("<");
|
|
874
|
+
if (firstAngle !== -1 && firstAngle < firstParen) {
|
|
875
|
+
methodName = stmt.slice(0, firstAngle).trim().replace(/^(?:async\s+)/, "");
|
|
876
|
+
let angDepth = 0;
|
|
877
|
+
let angClose = -1;
|
|
878
|
+
for (let j = firstAngle; j < firstParen; j++) {
|
|
879
|
+
if (stmt[j] === "<") angDepth++;
|
|
880
|
+
else if (stmt[j] === ">") {
|
|
881
|
+
angDepth--;
|
|
882
|
+
if (angDepth === 0) {
|
|
883
|
+
angClose = j;
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (angClose !== -1) {
|
|
889
|
+
typeParams = stmt.slice(firstAngle, angClose + 1).trim();
|
|
890
|
+
}
|
|
891
|
+
} else {
|
|
892
|
+
methodName = stmt.slice(0, firstParen).trim().replace(/^(?:async\s+)/, "");
|
|
893
|
+
}
|
|
894
|
+
if (!methodName || !/^[A-Za-z0-9_$]+$/.test(methodName)) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
let parenDepth = 0;
|
|
898
|
+
let parenClose = -1;
|
|
899
|
+
for (let j = firstParen; j < stmt.length; j++) {
|
|
900
|
+
if (stmt[j] === "(") parenDepth++;
|
|
901
|
+
else if (stmt[j] === ")") {
|
|
902
|
+
parenDepth--;
|
|
903
|
+
if (parenDepth === 0) {
|
|
904
|
+
parenClose = j;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (parenClose === -1) continue;
|
|
910
|
+
const rawParams = stmt.slice(firstParen + 1, parenClose).trim();
|
|
911
|
+
const params = rawParams.replace(/\s+/g, " ");
|
|
912
|
+
const afterParen = stmt.slice(parenClose + 1).trim();
|
|
913
|
+
let returnType = "void";
|
|
914
|
+
if (afterParen.startsWith(":")) {
|
|
915
|
+
returnType = afterParen.slice(1).trim().replace(/\s+/g, " ");
|
|
916
|
+
}
|
|
917
|
+
const isAsync = returnType.startsWith("Promise<") || returnType === "Promise";
|
|
918
|
+
methods.push({
|
|
919
|
+
name: methodName,
|
|
920
|
+
typeParams,
|
|
921
|
+
params,
|
|
922
|
+
returnType,
|
|
923
|
+
isAsync
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
className,
|
|
928
|
+
methods,
|
|
929
|
+
imports,
|
|
930
|
+
extendsClass
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function findInterfaceFile(projectRoot, ifaceName, featureName, customPath) {
|
|
934
|
+
if (customPath && fs3__default.default.existsSync(customPath)) {
|
|
935
|
+
return { filePath: customPath, ifaceName };
|
|
936
|
+
}
|
|
937
|
+
const candidateNames = [
|
|
938
|
+
ifaceName,
|
|
939
|
+
ensureInterfacePrefix(ifaceName),
|
|
940
|
+
removeInterfacePrefix(ifaceName)
|
|
941
|
+
];
|
|
942
|
+
const features = featureName ? [featureName, ...listExistingFeatures(projectRoot).filter((f) => f !== featureName)] : listExistingFeatures(projectRoot);
|
|
943
|
+
for (const feat of features) {
|
|
944
|
+
const domainDir = path7__default.default.join(resolveFeaturesDir(projectRoot), feat, "domain");
|
|
945
|
+
if (!fs3__default.default.existsSync(domainDir)) continue;
|
|
946
|
+
for (const cand of candidateNames) {
|
|
947
|
+
const p1 = path7__default.default.join(domainDir, `${cand}.ts`);
|
|
948
|
+
if (fs3__default.default.existsSync(p1)) {
|
|
949
|
+
return { filePath: p1, ifaceName: cand, feature: feat };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
for (const cand of candidateNames) {
|
|
954
|
+
const p1 = path7__default.default.join(projectRoot, "domain", `${cand}.ts`);
|
|
955
|
+
if (fs3__default.default.existsSync(p1)) return { filePath: p1, ifaceName: cand };
|
|
956
|
+
const p22 = path7__default.default.join(projectRoot, "src", "domain", `${cand}.ts`);
|
|
957
|
+
if (fs3__default.default.existsSync(p22)) return { filePath: p22, ifaceName: cand };
|
|
958
|
+
const p32 = path7__default.default.join(projectRoot, "interfaces", `${cand}.ts`);
|
|
959
|
+
if (fs3__default.default.existsSync(p32)) return { filePath: p32, ifaceName: cand };
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
function loadInterfaceHierarchy(projectRoot, initialIfaceName, featureName, customPath) {
|
|
964
|
+
const found = findInterfaceFile(projectRoot, initialIfaceName, featureName, customPath);
|
|
965
|
+
if (!found) {
|
|
966
|
+
return {
|
|
967
|
+
resolvedIfaceName: initialIfaceName,
|
|
968
|
+
methods: [],
|
|
969
|
+
imports: []
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
const visited = /* @__PURE__ */ new Set();
|
|
973
|
+
const allMethods = [];
|
|
974
|
+
const allImports = [];
|
|
975
|
+
let currentFile = found.filePath;
|
|
976
|
+
let currentName = found.ifaceName;
|
|
977
|
+
const primaryResolvedName = found.ifaceName;
|
|
978
|
+
const primaryFilePath = found.filePath;
|
|
979
|
+
while (currentFile && !visited.has(currentFile)) {
|
|
980
|
+
visited.add(currentFile);
|
|
981
|
+
try {
|
|
982
|
+
const content = fs3__default.default.readFileSync(currentFile, "utf-8");
|
|
983
|
+
const parsed = parseInterfaceMethodsAndImports(content, currentName);
|
|
984
|
+
for (const m of parsed.methods) {
|
|
985
|
+
if (!allMethods.some((existing) => existing.name === m.name)) {
|
|
986
|
+
allMethods.push(m);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
for (const imp of parsed.imports) {
|
|
990
|
+
allImports.push(imp);
|
|
991
|
+
}
|
|
992
|
+
if (parsed.extendsClass) {
|
|
993
|
+
const nextFound = findInterfaceFile(projectRoot, parsed.extendsClass, featureName);
|
|
994
|
+
if (nextFound) {
|
|
995
|
+
currentFile = nextFound.filePath;
|
|
996
|
+
currentName = nextFound.ifaceName;
|
|
997
|
+
} else {
|
|
998
|
+
currentFile = void 0;
|
|
999
|
+
}
|
|
1000
|
+
} else {
|
|
1001
|
+
currentFile = void 0;
|
|
1002
|
+
}
|
|
1003
|
+
} catch {
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
resolvedIfaceName: primaryResolvedName,
|
|
1009
|
+
ifaceFilePath: primaryFilePath,
|
|
1010
|
+
methods: allMethods,
|
|
1011
|
+
imports: allImports
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
function synthesizeCrudMethods(entityName) {
|
|
1015
|
+
const baseEntity = toPascalCase(entityName);
|
|
1016
|
+
const entityVar = baseEntity.charAt(0).toLowerCase() + baseEntity.slice(1);
|
|
1017
|
+
const entitiesVar = `${entityVar}s`;
|
|
1018
|
+
const idKey = `${entityVar}Id`;
|
|
1019
|
+
return [
|
|
1020
|
+
{
|
|
1021
|
+
name: `Get${baseEntity}ById`,
|
|
1022
|
+
params: `input: { ${idKey}: string }`,
|
|
1023
|
+
returnType: `Promise<{ ${entityVar}: ${baseEntity} }>`,
|
|
1024
|
+
isAsync: true
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
name: `List${baseEntity}s`,
|
|
1028
|
+
params: `input?: { page?: number | undefined; limit?: number | undefined }`,
|
|
1029
|
+
returnType: `Promise<{ ${entitiesVar}: ${baseEntity}[]; total?: number | undefined; page?: number | undefined; limit?: number | undefined }>`,
|
|
1030
|
+
isAsync: true
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
name: `Create${baseEntity}`,
|
|
1034
|
+
params: `input: { ${entityVar}: ${baseEntity} }`,
|
|
1035
|
+
returnType: `Promise<{ ${entityVar}: ${baseEntity} }>`,
|
|
1036
|
+
isAsync: true
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
name: `Update${baseEntity}`,
|
|
1040
|
+
params: `input: { ${entityVar}: ${baseEntity} }`,
|
|
1041
|
+
returnType: `Promise<{ ${entityVar}: ${baseEntity} }>`,
|
|
1042
|
+
isAsync: true
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
name: `Delete${baseEntity}`,
|
|
1046
|
+
params: `input: { ${idKey}: string }`,
|
|
1047
|
+
returnType: `Promise<{ success: boolean }>`,
|
|
1048
|
+
isAsync: true
|
|
1049
|
+
}
|
|
1050
|
+
];
|
|
1051
|
+
}
|
|
1052
|
+
function generateStubCode(stubClassName, ifaceName, entityName, featureName, methods = [], domainImports = [], ifaceImportPath) {
|
|
1053
|
+
const baseEntity = entityName ? toPascalCase(entityName) : toPascalCase(removeInterfacePrefix(ifaceName).replace(/Repository$|Service$|Client$/, ""));
|
|
1054
|
+
const entityVar = baseEntity ? baseEntity.charAt(0).toLowerCase() + baseEntity.slice(1) : "";
|
|
1055
|
+
const entitiesVar = `${entityVar}s`;
|
|
1056
|
+
const idKey = `${entityVar}Id`;
|
|
1057
|
+
const impPath = ifaceImportPath || `../domain/${ifaceName}.js`;
|
|
1058
|
+
const entityImportPath = ifaceImportPath ? `${path7__default.default.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
|
|
1059
|
+
const importsByFrom = /* @__PURE__ */ new Map();
|
|
1060
|
+
for (const imp of domainImports) {
|
|
1061
|
+
if (imp.name === ifaceName || imp.name === baseEntity) continue;
|
|
1062
|
+
if (!importsByFrom.has(imp.from)) {
|
|
1063
|
+
importsByFrom.set(imp.from, { typeOnly: imp.isTypeOnly ?? true, names: /* @__PURE__ */ new Set() });
|
|
1064
|
+
}
|
|
1065
|
+
const group = importsByFrom.get(imp.from);
|
|
1066
|
+
group.names.add(imp.name);
|
|
1067
|
+
if (imp.isTypeOnly === false) {
|
|
1068
|
+
group.typeOnly = false;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const formattedDomainImports = Array.from(importsByFrom.entries()).map(([from, group]) => ({
|
|
1072
|
+
from,
|
|
1073
|
+
names: Array.from(group.names).sort().join(", "),
|
|
1074
|
+
isTypeOnly: group.typeOnly
|
|
1075
|
+
}));
|
|
1076
|
+
const standardNames = /* @__PURE__ */ new Set([
|
|
1077
|
+
`Get${baseEntity}ById`,
|
|
1078
|
+
`List${baseEntity}s`,
|
|
1079
|
+
`Create${baseEntity}`,
|
|
1080
|
+
`Update${baseEntity}`,
|
|
1081
|
+
`Delete${baseEntity}`
|
|
1082
|
+
]);
|
|
1083
|
+
const extraMethods = methods.filter((m) => !standardNames.has(m.name));
|
|
1084
|
+
return renderTemplate(stubInfraTemplate, {
|
|
1085
|
+
stubClassName,
|
|
1086
|
+
ifaceName,
|
|
1087
|
+
ifaceImportPath: impPath,
|
|
1088
|
+
baseEntity: baseEntity || void 0,
|
|
1089
|
+
entityImportPath,
|
|
1090
|
+
domainImports: formattedDomainImports,
|
|
1091
|
+
entitiesVar,
|
|
1092
|
+
entityVar,
|
|
1093
|
+
idKey,
|
|
1094
|
+
extraMethods,
|
|
1095
|
+
hasMethods: methods.length > 0,
|
|
1096
|
+
methods
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
function generateConcreteInfraCode(className, ifaceName, dependencies = [], methods = [], domainImports = [], ifaceImportPath) {
|
|
1100
|
+
let finalMethods = [...methods];
|
|
1101
|
+
const finalDomainImports = [...domainImports];
|
|
1102
|
+
if (finalMethods.length === 0 && ifaceName) {
|
|
1103
|
+
const baseEntity = toPascalCase(removeInterfacePrefix(ifaceName).replace(/Repository$|Service$|Client$/, ""));
|
|
1104
|
+
if (baseEntity) {
|
|
1105
|
+
finalMethods = synthesizeCrudMethods(baseEntity);
|
|
1106
|
+
if (!finalDomainImports.some((imp) => imp.name === baseEntity)) {
|
|
1107
|
+
const importPath = ifaceImportPath ? `${path7__default.default.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
|
|
1108
|
+
finalDomainImports.push({
|
|
1109
|
+
name: baseEntity,
|
|
1110
|
+
from: importPath,
|
|
1111
|
+
isTypeOnly: true
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const impPath = ifaceName ? ifaceImportPath || `../domain/${ifaceName}.js` : "";
|
|
1117
|
+
const importsByFrom = /* @__PURE__ */ new Map();
|
|
1118
|
+
for (const imp of finalDomainImports) {
|
|
1119
|
+
if (imp.name === ifaceName) continue;
|
|
1120
|
+
if (!importsByFrom.has(imp.from)) {
|
|
1121
|
+
importsByFrom.set(imp.from, { typeOnly: imp.isTypeOnly ?? true, names: /* @__PURE__ */ new Set() });
|
|
1122
|
+
}
|
|
1123
|
+
const group = importsByFrom.get(imp.from);
|
|
1124
|
+
group.names.add(imp.name);
|
|
1125
|
+
if (imp.isTypeOnly === false) {
|
|
1126
|
+
group.typeOnly = false;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const formattedDomainImports = Array.from(importsByFrom.entries()).map(([from, group]) => ({
|
|
1130
|
+
from,
|
|
1131
|
+
names: Array.from(group.names).sort().join(", "),
|
|
1132
|
+
isTypeOnly: group.typeOnly
|
|
1133
|
+
}));
|
|
1134
|
+
const formattedDeps = dependencies.map((dep) => ({
|
|
1135
|
+
key: dep.charAt(0).toLowerCase() + dep.slice(1),
|
|
1136
|
+
name: dep
|
|
1137
|
+
}));
|
|
1138
|
+
return renderTemplate(concreteInfraTemplate, {
|
|
1139
|
+
className,
|
|
1140
|
+
ifaceName,
|
|
1141
|
+
ifaceImportPath: impPath,
|
|
1142
|
+
domainImports: formattedDomainImports,
|
|
1143
|
+
dependencies: formattedDeps,
|
|
1144
|
+
hasMethods: finalMethods.length > 0,
|
|
1145
|
+
methods: finalMethods
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
function generateInfrastructure(options) {
|
|
1149
|
+
const projectRoot = options.projectRoot || findProjectRoot();
|
|
1150
|
+
const className = toPascalCase(options.name);
|
|
1151
|
+
const isFeatureScope = options.scope !== "global" && Boolean(options.feature);
|
|
1152
|
+
const targetDir = options.targetDir || (isFeatureScope ? path7__default.default.join(resolveFeaturesDir(projectRoot), options.feature, "infrastructure") : path7__default.default.join(projectRoot, "infrastructure"));
|
|
1153
|
+
const filePath = path7__default.default.join(targetDir, `${className}.ts`);
|
|
1154
|
+
let code;
|
|
1155
|
+
if (options.implementsInterface) {
|
|
1156
|
+
const rawIfaceName = options.implementsInterface;
|
|
1157
|
+
const hierarchy = loadInterfaceHierarchy(
|
|
1158
|
+
projectRoot,
|
|
1159
|
+
rawIfaceName,
|
|
1160
|
+
options.feature,
|
|
1161
|
+
options.interfacePath
|
|
1162
|
+
);
|
|
1163
|
+
const ifaceName = hierarchy.resolvedIfaceName;
|
|
1164
|
+
const ifaceFilePath = hierarchy.ifaceFilePath;
|
|
1165
|
+
let ifaceImportPath;
|
|
1166
|
+
if (ifaceFilePath) {
|
|
1167
|
+
let rel = path7__default.default.relative(targetDir, ifaceFilePath).replace(/\\/g, "/").replace(/\.ts$/, ".js");
|
|
1168
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
1169
|
+
ifaceImportPath = rel;
|
|
1170
|
+
} else if (isFeatureScope) {
|
|
1171
|
+
ifaceImportPath = `../domain/${ifaceName}.js`;
|
|
1172
|
+
}
|
|
1173
|
+
const allSignatures = hierarchy.methods.map((m) => `${m.params} ${m.returnType}`).join(" ");
|
|
1174
|
+
const domainImports = [];
|
|
1175
|
+
for (const imp of hierarchy.imports) {
|
|
1176
|
+
for (const item of imp.items) {
|
|
1177
|
+
if (new RegExp(`\\b${item.name}\\b`).test(allSignatures)) {
|
|
1178
|
+
let fromPath;
|
|
1179
|
+
if (imp.specifier.startsWith(".") && ifaceFilePath) {
|
|
1180
|
+
const absTarget = path7__default.default.resolve(path7__default.default.dirname(ifaceFilePath), imp.specifier);
|
|
1181
|
+
let rel = path7__default.default.relative(targetDir, absTarget).replace(/\\/g, "/");
|
|
1182
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
1183
|
+
fromPath = rel;
|
|
1184
|
+
} else {
|
|
1185
|
+
fromPath = imp.specifier;
|
|
1186
|
+
}
|
|
1187
|
+
domainImports.push({
|
|
1188
|
+
name: item.name,
|
|
1189
|
+
from: fromPath,
|
|
1190
|
+
isTypeOnly: item.isTypeOnly ?? imp.isTypeOnly ?? true
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (options.kind === "stub") {
|
|
1196
|
+
code = generateStubCode(
|
|
1197
|
+
className,
|
|
1198
|
+
ifaceName,
|
|
1199
|
+
options.entityName,
|
|
1200
|
+
options.feature,
|
|
1201
|
+
hierarchy.methods,
|
|
1202
|
+
domainImports,
|
|
1203
|
+
ifaceImportPath
|
|
1204
|
+
);
|
|
1205
|
+
} else {
|
|
1206
|
+
code = generateConcreteInfraCode(
|
|
1207
|
+
className,
|
|
1208
|
+
ifaceName,
|
|
1209
|
+
options.dependencies || [],
|
|
1210
|
+
hierarchy.methods,
|
|
1211
|
+
domainImports,
|
|
1212
|
+
ifaceImportPath
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
} else {
|
|
1216
|
+
code = generateConcreteInfraCode(
|
|
1217
|
+
className,
|
|
1218
|
+
void 0,
|
|
1219
|
+
options.dependencies || []
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
return writeFileSafely(filePath, code, {
|
|
1223
|
+
projectRoot,
|
|
1224
|
+
dryRun: options.dryRun,
|
|
1225
|
+
overwrite: options.overwrite
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
var primitiveTypes = /* @__PURE__ */ new Set([
|
|
1229
|
+
"string",
|
|
1230
|
+
"number",
|
|
1231
|
+
"boolean",
|
|
1232
|
+
"bigint",
|
|
1233
|
+
"symbol",
|
|
1234
|
+
"undefined",
|
|
1235
|
+
"null",
|
|
1236
|
+
"any",
|
|
1237
|
+
"unknown",
|
|
1238
|
+
"never",
|
|
1239
|
+
"void",
|
|
1240
|
+
"Date",
|
|
1241
|
+
"Record<string, any>",
|
|
1242
|
+
"Record<string, unknown>",
|
|
1243
|
+
"Record<string, never>"
|
|
1244
|
+
]);
|
|
1245
|
+
function generateUseCaseCode(useCaseName, dependencies = [], inputProps = [], outputProps = [], domainImports = []) {
|
|
1246
|
+
const ucName = toPascalCase(useCaseName);
|
|
1247
|
+
const inputTypeName = `${ucName}Input`;
|
|
1248
|
+
const outputTypeName = `${ucName}Output`;
|
|
1249
|
+
const dependencyImports = dependencies.map((dep) => ({
|
|
1250
|
+
name: dep,
|
|
1251
|
+
from: /^I[A-Z]/.test(dep) ? `../domain/${dep}.js` : `./${dep}.js`
|
|
1252
|
+
}));
|
|
1253
|
+
const domainTypesToImport = new Set(domainImports);
|
|
1254
|
+
for (const p15 of [...inputProps, ...outputProps]) {
|
|
1255
|
+
const rawType = p15.type.trim();
|
|
1256
|
+
const baseType = rawType.replace(/\[\]$/, "").trim();
|
|
1257
|
+
if (!primitiveTypes.has(baseType) && /^[A-Z][A-Za-z0-9_]*$/.test(baseType) && !dependencies.includes(baseType)) {
|
|
1258
|
+
domainTypesToImport.add(baseType);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
const formattedInputProps = inputProps.map((p15) => ({
|
|
1262
|
+
name: p15.name,
|
|
1263
|
+
optional: p15.optional,
|
|
1264
|
+
type: p15.optional && !p15.type.includes("undefined") ? `${p15.type} | undefined` : p15.type
|
|
1265
|
+
}));
|
|
1266
|
+
const formattedOutputProps = outputProps.map((p15) => ({
|
|
1267
|
+
name: p15.name,
|
|
1268
|
+
optional: p15.optional,
|
|
1269
|
+
type: p15.optional && !p15.type.includes("undefined") ? `${p15.type} | undefined` : p15.type
|
|
1270
|
+
}));
|
|
1271
|
+
const formattedDeps = dependencies.map((dep) => ({
|
|
1272
|
+
key: toCamelCase(dep),
|
|
1273
|
+
name: dep
|
|
1274
|
+
}));
|
|
1275
|
+
const getMatch = ucName.match(/^Get([A-Z][A-Za-z0-9]*)$/);
|
|
1276
|
+
const listMatch = ucName.match(/^List([A-Z][A-Za-z0-9]*)$/);
|
|
1277
|
+
const createMatch = ucName.match(/^Create([A-Z][A-Za-z0-9]*)$/);
|
|
1278
|
+
const updateMatch = ucName.match(/^Update([A-Z][A-Za-z0-9]*)$/);
|
|
1279
|
+
const deleteMatch = ucName.match(/^Delete([A-Z][A-Za-z0-9]*)$/);
|
|
1280
|
+
let callWired = null;
|
|
1281
|
+
if (getMatch && getMatch[1]) {
|
|
1282
|
+
const entity = getMatch[1];
|
|
1283
|
+
const matchingRepo = dependencies.find((d) => d === `I${entity}Repository`);
|
|
1284
|
+
if (matchingRepo && inputProps.length === 1 && outputProps.length === 1) {
|
|
1285
|
+
const repoKey = toCamelCase(matchingRepo);
|
|
1286
|
+
const idKey = inputProps[0].name;
|
|
1287
|
+
callWired = ` return await this.deps.${repoKey}.Get${entity}ById({
|
|
1288
|
+
${idKey}: props.${idKey},
|
|
1289
|
+
});`;
|
|
1290
|
+
}
|
|
1291
|
+
} else if (listMatch && listMatch[1]) {
|
|
1292
|
+
const pluralEntity = listMatch[1];
|
|
1293
|
+
const singularEntity = toSingular(pluralEntity);
|
|
1294
|
+
const matchingRepo = dependencies.find(
|
|
1295
|
+
(d) => d === `I${singularEntity}Repository` || d === `I${pluralEntity}Repository`
|
|
1296
|
+
);
|
|
1297
|
+
if (matchingRepo) {
|
|
1298
|
+
const repoKey = toCamelCase(matchingRepo);
|
|
1299
|
+
const passProps = inputProps.length > 0 ? "props" : "";
|
|
1300
|
+
callWired = ` return await this.deps.${repoKey}.List${pluralEntity}(${passProps});`;
|
|
1301
|
+
}
|
|
1302
|
+
} else if (createMatch && createMatch[1]) {
|
|
1303
|
+
const entity = createMatch[1];
|
|
1304
|
+
const matchingRepo = dependencies.find((d) => d === `I${entity}Repository`);
|
|
1305
|
+
if (matchingRepo && inputProps.length === 1 && outputProps.length === 1) {
|
|
1306
|
+
const repoKey = toCamelCase(matchingRepo);
|
|
1307
|
+
const entityKey = inputProps[0].name;
|
|
1308
|
+
callWired = ` return await this.deps.${repoKey}.Create${entity}({
|
|
1309
|
+
${entityKey}: props.${entityKey},
|
|
1310
|
+
});`;
|
|
1311
|
+
}
|
|
1312
|
+
} else if (updateMatch && updateMatch[1]) {
|
|
1313
|
+
const entity = updateMatch[1];
|
|
1314
|
+
const matchingRepo = dependencies.find((d) => d === `I${entity}Repository`);
|
|
1315
|
+
if (matchingRepo && inputProps.length === 1 && outputProps.length === 1) {
|
|
1316
|
+
const repoKey = toCamelCase(matchingRepo);
|
|
1317
|
+
const entityKey = inputProps[0].name;
|
|
1318
|
+
callWired = ` return await this.deps.${repoKey}.Update${entity}({
|
|
1319
|
+
${entityKey}: props.${entityKey},
|
|
1320
|
+
});`;
|
|
1321
|
+
}
|
|
1322
|
+
} else if (deleteMatch && deleteMatch[1]) {
|
|
1323
|
+
const entity = deleteMatch[1];
|
|
1324
|
+
const matchingRepo = dependencies.find((d) => d === `I${entity}Repository`);
|
|
1325
|
+
if (matchingRepo && inputProps.length === 1) {
|
|
1326
|
+
const repoKey = toCamelCase(matchingRepo);
|
|
1327
|
+
const idKey = inputProps[0].name;
|
|
1328
|
+
callWired = ` return await this.deps.${repoKey}.Delete${entity}({
|
|
1329
|
+
${idKey}: props.${idKey},
|
|
1330
|
+
});`;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const paramStr = inputProps.length > 0 ? `props: ${inputTypeName}` : `_props?: ${inputTypeName}`;
|
|
1334
|
+
return renderTemplate(useCaseTemplate, {
|
|
1335
|
+
ucName,
|
|
1336
|
+
inputTypeName,
|
|
1337
|
+
outputTypeName,
|
|
1338
|
+
dependencyImports,
|
|
1339
|
+
domainImports: Array.from(domainTypesToImport),
|
|
1340
|
+
hasInputProps: inputProps.length > 0,
|
|
1341
|
+
inputProperties: formattedInputProps,
|
|
1342
|
+
hasOutputProps: outputProps.length > 0,
|
|
1343
|
+
outputProperties: formattedOutputProps,
|
|
1344
|
+
dependencies: formattedDeps,
|
|
1345
|
+
hasDependencies: dependencies.length > 0,
|
|
1346
|
+
paramStr,
|
|
1347
|
+
callWired
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
function generateUseCaseTestCode(useCaseName, dependencies = []) {
|
|
1351
|
+
const ucName = toPascalCase(useCaseName);
|
|
1352
|
+
const stubBindings = [];
|
|
1353
|
+
for (const dep of dependencies) {
|
|
1354
|
+
if (/^I[A-Z]/.test(dep)) {
|
|
1355
|
+
const stub = toStubName(dep);
|
|
1356
|
+
stubBindings.push({ iface: dep, stub });
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return renderTemplate(useCaseTestTemplate, {
|
|
1360
|
+
ucName,
|
|
1361
|
+
stubBindings
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
function generateUseCase(options) {
|
|
1365
|
+
const files = [];
|
|
1366
|
+
const projectRoot = options.projectRoot || findProjectRoot();
|
|
1367
|
+
const ucName = toPascalCase(options.name);
|
|
1368
|
+
const targetDir = options.targetDir || path7__default.default.join(resolveFeaturesDir(projectRoot), options.feature, "useCases");
|
|
1369
|
+
const filePath = path7__default.default.join(targetDir, `${ucName}.ts`);
|
|
1370
|
+
const code = generateUseCaseCode(
|
|
1371
|
+
ucName,
|
|
1372
|
+
options.dependencies || [],
|
|
1373
|
+
options.inputProperties || [],
|
|
1374
|
+
options.outputProperties || [],
|
|
1375
|
+
options.domainImports || []
|
|
1376
|
+
);
|
|
1377
|
+
files.push(
|
|
1378
|
+
writeFileSafely(filePath, code, {
|
|
1379
|
+
projectRoot,
|
|
1380
|
+
dryRun: options.dryRun,
|
|
1381
|
+
overwrite: options.overwrite
|
|
1382
|
+
})
|
|
1383
|
+
);
|
|
1384
|
+
if (options.includeTest) {
|
|
1385
|
+
const testPath = path7__default.default.join(targetDir, `${ucName}.test.ts`);
|
|
1386
|
+
const testCode = generateUseCaseTestCode(ucName, options.dependencies || []);
|
|
1387
|
+
files.push(
|
|
1388
|
+
writeFileSafely(testPath, testCode, {
|
|
1389
|
+
projectRoot,
|
|
1390
|
+
dryRun: options.dryRun,
|
|
1391
|
+
overwrite: options.overwrite
|
|
1392
|
+
})
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
return files;
|
|
1396
|
+
}
|
|
1397
|
+
function deriveSuccessMessage(handlerName, useCaseName, method = "get") {
|
|
1398
|
+
const baseName = useCaseName ? toPascalCase(useCaseName) : toPascalCase(handlerName).replace(/Http$/, "");
|
|
1399
|
+
const createMatch = baseName.match(/^Create([A-Z].*)$/);
|
|
1400
|
+
if (createMatch) return `${createMatch[1]} created successfully`;
|
|
1401
|
+
const getMatch = baseName.match(/^(?:Get|Find)([A-Z].*)$/);
|
|
1402
|
+
if (getMatch) return `${getMatch[1]} retrieved successfully`;
|
|
1403
|
+
const listMatch = baseName.match(/^List([A-Z].*)$/);
|
|
1404
|
+
if (listMatch) return `${listMatch[1]} retrieved successfully`;
|
|
1405
|
+
const updateMatch = baseName.match(/^(?:Update|Modify|Patch)([A-Z].*)$/);
|
|
1406
|
+
if (updateMatch) return `${updateMatch[1]} updated successfully`;
|
|
1407
|
+
const deleteMatch = baseName.match(/^(?:Delete|Remove)([A-Z].*)$/);
|
|
1408
|
+
if (deleteMatch) return `${deleteMatch[1]} deleted successfully`;
|
|
1409
|
+
if (method === "post") return `${baseName} created successfully`;
|
|
1410
|
+
if (method === "delete") return `${baseName} deleted successfully`;
|
|
1411
|
+
return `${baseName} executed successfully`;
|
|
1412
|
+
}
|
|
1413
|
+
function generateHttpHandlerCode(handlerName, method = "get", routeSubPath = "/", useCaseName, includeValidation = false, validationSource = "body", validationFields = [], customSuccessMessage) {
|
|
1414
|
+
const className = toPascalCase(handlerName);
|
|
1415
|
+
const ucName = useCaseName ? toPascalCase(useCaseName) : void 0;
|
|
1416
|
+
const ucDepKey = ucName ? toCamelCase(ucName) : void 0;
|
|
1417
|
+
const schemaName = `${toCamelCase(className)}Schema`;
|
|
1418
|
+
const formattedFields = validationFields.map((f) => {
|
|
1419
|
+
let zodType = "z.string()";
|
|
1420
|
+
if (f.type === "number") zodType = validationSource === "query" ? "z.coerce.number()" : "z.number()";
|
|
1421
|
+
else if (f.type === "boolean") zodType = validationSource === "query" ? "z.coerce.boolean()" : "z.boolean()";
|
|
1422
|
+
else if (f.type === "Date") zodType = "z.coerce.date()";
|
|
1423
|
+
else if (f.type === "any") zodType = "z.any()";
|
|
1424
|
+
else if (f.type === "unknown") zodType = "z.unknown()";
|
|
1425
|
+
else if (f.type.endsWith("[]")) zodType = "z.array(z.any())";
|
|
1426
|
+
else if (/^[A-Z]/.test(f.type)) zodType = "z.record(z.any()) as any";
|
|
1427
|
+
if (f.optional) zodType += ".optional()";
|
|
1428
|
+
return { name: f.name, zodType };
|
|
1429
|
+
});
|
|
1430
|
+
const callArg = includeValidation ? "parsed.data" : "{}";
|
|
1431
|
+
const statusCode = method === "post" ? 201 : 200;
|
|
1432
|
+
const successMessage = customSuccessMessage || deriveSuccessMessage(className, ucName, method);
|
|
1433
|
+
return renderTemplate(httpHandlerTemplate, {
|
|
1434
|
+
className,
|
|
1435
|
+
ucName,
|
|
1436
|
+
ucDepKey,
|
|
1437
|
+
includeValidation,
|
|
1438
|
+
schemaName,
|
|
1439
|
+
hasValidationFields: formattedFields.length > 0,
|
|
1440
|
+
validationFields: formattedFields,
|
|
1441
|
+
method,
|
|
1442
|
+
routeSubPath,
|
|
1443
|
+
validationSource,
|
|
1444
|
+
callArg,
|
|
1445
|
+
statusCode,
|
|
1446
|
+
successMessage
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
function generateHttpHandler(options) {
|
|
1450
|
+
const files = [];
|
|
1451
|
+
const projectRoot = options.projectRoot || findProjectRoot();
|
|
1452
|
+
const className = toPascalCase(options.name);
|
|
1453
|
+
const pxDir = path7__default.default.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress");
|
|
1454
|
+
const handlersDir = path7__default.default.join(pxDir, "handlers");
|
|
1455
|
+
const pxIndexPath = path7__default.default.join(pxDir, "index.ts");
|
|
1456
|
+
if (!fs3__default.default.existsSync(pxIndexPath)) {
|
|
1457
|
+
const featurePath = options.routePrefix || `/${options.feature.toLowerCase()}`;
|
|
1458
|
+
const pxIndexContent = renderTemplate(expressIndexTemplate, {
|
|
1459
|
+
routePath: featurePath
|
|
1460
|
+
});
|
|
1461
|
+
files.push(
|
|
1462
|
+
writeFileSafely(pxIndexPath, pxIndexContent, {
|
|
1463
|
+
projectRoot,
|
|
1464
|
+
dryRun: options.dryRun,
|
|
1465
|
+
overwrite: false
|
|
1466
|
+
})
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
const handlerFilePath = path7__default.default.join(handlersDir, `${className}.ts`);
|
|
1470
|
+
const code = generateHttpHandlerCode(
|
|
1471
|
+
className,
|
|
1472
|
+
options.method || "get",
|
|
1473
|
+
options.path || "/",
|
|
1474
|
+
options.useCaseName,
|
|
1475
|
+
Boolean(options.includeValidation),
|
|
1476
|
+
options.validationSource || "body",
|
|
1477
|
+
options.validationFields || [],
|
|
1478
|
+
options.successMessage
|
|
1479
|
+
);
|
|
1480
|
+
files.push(
|
|
1481
|
+
writeFileSafely(handlerFilePath, code, {
|
|
1482
|
+
projectRoot,
|
|
1483
|
+
dryRun: options.dryRun,
|
|
1484
|
+
overwrite: options.overwrite
|
|
1485
|
+
})
|
|
1486
|
+
);
|
|
1487
|
+
return files;
|
|
1488
|
+
}
|
|
1489
|
+
function updateOrGenerateDiProvider(featureDir, featureName, bindings = [], options = {}) {
|
|
1490
|
+
const providerPath = path7__default.default.join(featureDir, "diProvider.ts");
|
|
1491
|
+
const pascalFeature = toPascalCase(featureName);
|
|
1492
|
+
const providerModuleName = `${pascalFeature}Provider`;
|
|
1493
|
+
if (fs3__default.default.existsSync(providerPath) && !options.overwrite) {
|
|
1494
|
+
let content = fs3__default.default.readFileSync(providerPath, "utf-8");
|
|
1495
|
+
content = content.replace(/\n*export\s+const\s+ScaffoldProvider\s*=[^;]+;?/g, "");
|
|
1496
|
+
for (const b of bindings) {
|
|
1497
|
+
const ifaceImp = b.ifaceImportPath || `./domain/${b.ifaceName}.js`;
|
|
1498
|
+
const implImp = b.implImportPath || `./infrastructure/${b.implName}.js`;
|
|
1499
|
+
if (!content.includes(b.ifaceName)) {
|
|
1500
|
+
content = `import { ${b.ifaceName} } from "${ifaceImp}";
|
|
1501
|
+
` + content;
|
|
1502
|
+
}
|
|
1503
|
+
if (!content.includes(b.implName)) {
|
|
1504
|
+
content = `import { ${b.implName} } from "${implImp}";
|
|
1505
|
+
` + content;
|
|
1506
|
+
}
|
|
1507
|
+
const provideCall = `c.provide(${b.ifaceName}, ${b.implName});`;
|
|
1508
|
+
if (!content.includes(b.ifaceName) || !content.includes(b.implName) || !content.includes(`c.provide(${b.ifaceName}`)) {
|
|
1509
|
+
const match = content.match(/(const\s+\w+Provider\s*:\s*DIModule\s*=\s*\((?:c|container)[^)]*\)\s*=>\s*\{)([\s\S]*?)(\}\;?)/);
|
|
1510
|
+
if (match) {
|
|
1511
|
+
const prefix = match[1];
|
|
1512
|
+
const body = match[2];
|
|
1513
|
+
const suffix = match[3];
|
|
1514
|
+
const updatedBody = `${body}
|
|
1515
|
+
${provideCall}
|
|
1516
|
+
`;
|
|
1517
|
+
content = content.replace(match[0], `${prefix}${updatedBody}${suffix}`);
|
|
1518
|
+
} else {
|
|
1519
|
+
content = content.replace(
|
|
1520
|
+
/export default/g,
|
|
1521
|
+
`// Register ${b.ifaceName}
|
|
1522
|
+
// c.provide(${b.ifaceName}, ${b.implName});
|
|
1523
|
+
|
|
1524
|
+
export default`
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
return writeFileSafely(providerPath, content, {
|
|
1530
|
+
...options,
|
|
1531
|
+
overwrite: true
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
const formattedBindings = bindings.map((b) => ({
|
|
1535
|
+
ifaceName: b.ifaceName,
|
|
1536
|
+
implName: b.implName,
|
|
1537
|
+
ifaceImportPath: b.ifaceImportPath || `./domain/${b.ifaceName}.js`,
|
|
1538
|
+
implImportPath: b.implImportPath || `./infrastructure/${b.implName}.js`
|
|
1539
|
+
}));
|
|
1540
|
+
const code = renderTemplate(diProviderTemplate, {
|
|
1541
|
+
providerModuleName,
|
|
1542
|
+
bindings: formattedBindings
|
|
1543
|
+
});
|
|
1544
|
+
return writeFileSafely(providerPath, code, options);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/core/featureGenerator.ts
|
|
1548
|
+
function generateFeature(options) {
|
|
1549
|
+
const files = [];
|
|
1550
|
+
const projectRoot = options.projectRoot || findProjectRoot();
|
|
1551
|
+
const rawName = options.name.trim();
|
|
1552
|
+
const featureName = toCamelCase(rawName);
|
|
1553
|
+
const targetDir = options.targetDir || path7__default.default.join(resolveFeaturesDir(projectRoot), featureName);
|
|
1554
|
+
const routePath = options.routePath || `/${featureName}`;
|
|
1555
|
+
const rawEntities = [];
|
|
1556
|
+
if (options.entityNames && options.entityNames.length > 0) {
|
|
1557
|
+
rawEntities.push(...options.entityNames);
|
|
1558
|
+
} else if (options.entityName) {
|
|
1559
|
+
rawEntities.push(options.entityName);
|
|
1560
|
+
} else {
|
|
1561
|
+
rawEntities.push(toSingular(rawName));
|
|
1562
|
+
}
|
|
1563
|
+
const entities = Array.from(
|
|
1564
|
+
new Set(rawEntities.map((e) => toPascalCase(e.trim())).filter(Boolean))
|
|
1565
|
+
);
|
|
1566
|
+
if (entities.length === 0) {
|
|
1567
|
+
entities.push(toPascalCase(toSingular(rawName)));
|
|
1568
|
+
}
|
|
1569
|
+
if (options.includeDomain !== false) {
|
|
1570
|
+
for (const ent of entities) {
|
|
1571
|
+
const domainFile = generateDomain({
|
|
1572
|
+
name: ent,
|
|
1573
|
+
feature: featureName,
|
|
1574
|
+
targetDir: path7__default.default.join(targetDir, "domain"),
|
|
1575
|
+
projectRoot,
|
|
1576
|
+
dryRun: options.dryRun,
|
|
1577
|
+
overwrite: options.overwrite,
|
|
1578
|
+
kind: "interface",
|
|
1579
|
+
properties: [
|
|
1580
|
+
{ name: "id", type: "string" },
|
|
1581
|
+
{ name: "createdAt", type: "Date" },
|
|
1582
|
+
{ name: "updatedAt", type: "Date" }
|
|
1583
|
+
]
|
|
1584
|
+
});
|
|
1585
|
+
files.push(domainFile);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (options.includeInterface !== false) {
|
|
1589
|
+
for (const ent of entities) {
|
|
1590
|
+
const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${ent}Repository`;
|
|
1591
|
+
const ifaceFile = generateInterface({
|
|
1592
|
+
name: ifaceName,
|
|
1593
|
+
feature: featureName,
|
|
1594
|
+
targetDir: path7__default.default.join(targetDir, "domain"),
|
|
1595
|
+
entityName: ent,
|
|
1596
|
+
projectRoot,
|
|
1597
|
+
dryRun: options.dryRun,
|
|
1598
|
+
overwrite: options.overwrite,
|
|
1599
|
+
methodsPreset: "crud"
|
|
1600
|
+
});
|
|
1601
|
+
files.push(ifaceFile);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (options.includeInfra !== false && options.includeInterface !== false) {
|
|
1605
|
+
for (const ent of entities) {
|
|
1606
|
+
const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${ent}Repository`;
|
|
1607
|
+
const stubName = options.stubName && entities.length === 1 ? toPascalCase(options.stubName) : `Stub${ent}Repository`;
|
|
1608
|
+
const stubFile = generateInfrastructure({
|
|
1609
|
+
name: stubName,
|
|
1610
|
+
feature: featureName,
|
|
1611
|
+
targetDir: path7__default.default.join(targetDir, "infrastructure"),
|
|
1612
|
+
kind: "stub",
|
|
1613
|
+
implementsInterface: ifaceName,
|
|
1614
|
+
entityName: ent,
|
|
1615
|
+
projectRoot,
|
|
1616
|
+
dryRun: options.dryRun,
|
|
1617
|
+
overwrite: options.overwrite
|
|
1618
|
+
});
|
|
1619
|
+
files.push(stubFile);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (options.includeDiProvider !== false && options.includeInterface !== false && options.includeInfra !== false) {
|
|
1623
|
+
const bindings = entities.map((ent) => {
|
|
1624
|
+
const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${ent}Repository`;
|
|
1625
|
+
const stubName = options.stubName && entities.length === 1 ? toPascalCase(options.stubName) : `Stub${ent}Repository`;
|
|
1626
|
+
return { ifaceName, implName: stubName };
|
|
1627
|
+
});
|
|
1628
|
+
const providerFile = updateOrGenerateDiProvider(targetDir, featureName, bindings, {
|
|
1629
|
+
projectRoot,
|
|
1630
|
+
dryRun: options.dryRun,
|
|
1631
|
+
overwrite: options.overwrite
|
|
1632
|
+
});
|
|
1633
|
+
files.push(providerFile);
|
|
1634
|
+
}
|
|
1635
|
+
if (options.includeUseCase !== false) {
|
|
1636
|
+
let ops = [];
|
|
1637
|
+
if (options.crudOperations) {
|
|
1638
|
+
ops = options.crudOperations;
|
|
1639
|
+
} else if (options.useCaseName) {
|
|
1640
|
+
ops = [];
|
|
1641
|
+
} else {
|
|
1642
|
+
ops = ["create", "read", "list", "update", "delete"];
|
|
1643
|
+
}
|
|
1644
|
+
if (options.useCaseName && ops.length === 0) {
|
|
1645
|
+
const primaryEntity = entities[0];
|
|
1646
|
+
const entVar = primaryEntity.charAt(0).toLowerCase() + primaryEntity.slice(1);
|
|
1647
|
+
const idParam = `${entVar}Id`;
|
|
1648
|
+
const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${primaryEntity}Repository`;
|
|
1649
|
+
const ucName = toPascalCase(options.useCaseName);
|
|
1650
|
+
const ucFiles = generateUseCase({
|
|
1651
|
+
name: ucName,
|
|
1652
|
+
feature: featureName,
|
|
1653
|
+
targetDir: path7__default.default.join(targetDir, "useCases"),
|
|
1654
|
+
dependencies: options.includeInterface !== false ? [ifaceName] : [],
|
|
1655
|
+
inputProperties: [{ name: idParam, type: "string" }],
|
|
1656
|
+
outputProperties: [{ name: entVar, type: primaryEntity }],
|
|
1657
|
+
domainImports: options.includeDomain !== false ? [primaryEntity] : [],
|
|
1658
|
+
includeTest: options.includeTests !== false,
|
|
1659
|
+
projectRoot,
|
|
1660
|
+
dryRun: options.dryRun,
|
|
1661
|
+
overwrite: options.overwrite
|
|
1662
|
+
});
|
|
1663
|
+
files.push(...ucFiles);
|
|
1664
|
+
if (options.includeExpress !== false) {
|
|
1665
|
+
const httpName = options.httpHandlerName ? toPascalCase(options.httpHandlerName) : `${ucName}Http`;
|
|
1666
|
+
const httpFiles = generateHttpHandler({
|
|
1667
|
+
name: httpName,
|
|
1668
|
+
feature: featureName,
|
|
1669
|
+
routePrefix: routePath,
|
|
1670
|
+
method: "get",
|
|
1671
|
+
path: `/:${idParam}`,
|
|
1672
|
+
useCaseName: ucName,
|
|
1673
|
+
includeValidation: true,
|
|
1674
|
+
validationSource: "params",
|
|
1675
|
+
validationFields: [{ name: idParam, type: "string" }],
|
|
1676
|
+
projectRoot,
|
|
1677
|
+
dryRun: options.dryRun,
|
|
1678
|
+
overwrite: options.overwrite
|
|
1679
|
+
});
|
|
1680
|
+
files.push(...httpFiles);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
for (let i = 0; i < entities.length; i++) {
|
|
1684
|
+
const ent = entities[i];
|
|
1685
|
+
const entVar = ent.charAt(0).toLowerCase() + ent.slice(1);
|
|
1686
|
+
const idParam = `${entVar}Id`;
|
|
1687
|
+
const listVar = toPlural(entVar);
|
|
1688
|
+
const pluralEntity = toPlural(ent);
|
|
1689
|
+
const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${ent}Repository`;
|
|
1690
|
+
const deps = options.includeInterface !== false ? [ifaceName] : [];
|
|
1691
|
+
const entitySubRoute = i === 0 ? "" : `/${toKebabCase(listVar)}`;
|
|
1692
|
+
for (const op of ops) {
|
|
1693
|
+
if (op === "create") {
|
|
1694
|
+
const ucName = `Create${ent}`;
|
|
1695
|
+
const ucFiles = generateUseCase({
|
|
1696
|
+
name: ucName,
|
|
1697
|
+
feature: featureName,
|
|
1698
|
+
targetDir: path7__default.default.join(targetDir, "useCases"),
|
|
1699
|
+
dependencies: deps,
|
|
1700
|
+
inputProperties: [{ name: entVar, type: ent }],
|
|
1701
|
+
outputProperties: [{ name: entVar, type: ent }],
|
|
1702
|
+
domainImports: options.includeDomain !== false ? [ent] : [],
|
|
1703
|
+
includeTest: options.includeTests !== false,
|
|
1704
|
+
projectRoot,
|
|
1705
|
+
dryRun: options.dryRun,
|
|
1706
|
+
overwrite: options.overwrite
|
|
1707
|
+
});
|
|
1708
|
+
files.push(...ucFiles);
|
|
1709
|
+
if (options.includeExpress !== false) {
|
|
1710
|
+
const httpFiles = generateHttpHandler({
|
|
1711
|
+
name: `${ucName}Http`,
|
|
1712
|
+
feature: featureName,
|
|
1713
|
+
routePrefix: routePath,
|
|
1714
|
+
method: "post",
|
|
1715
|
+
path: entitySubRoute || "/",
|
|
1716
|
+
useCaseName: ucName,
|
|
1717
|
+
includeValidation: true,
|
|
1718
|
+
validationSource: "body",
|
|
1719
|
+
validationFields: [{ name: entVar, type: "any" }],
|
|
1720
|
+
projectRoot,
|
|
1721
|
+
dryRun: options.dryRun,
|
|
1722
|
+
overwrite: options.overwrite
|
|
1723
|
+
});
|
|
1724
|
+
files.push(...httpFiles);
|
|
1725
|
+
}
|
|
1726
|
+
} else if (op === "read") {
|
|
1727
|
+
const ucName = `Get${ent}`;
|
|
1728
|
+
const ucFiles = generateUseCase({
|
|
1729
|
+
name: ucName,
|
|
1730
|
+
feature: featureName,
|
|
1731
|
+
targetDir: path7__default.default.join(targetDir, "useCases"),
|
|
1732
|
+
dependencies: deps,
|
|
1733
|
+
inputProperties: [{ name: idParam, type: "string" }],
|
|
1734
|
+
outputProperties: [{ name: entVar, type: ent }],
|
|
1735
|
+
domainImports: options.includeDomain !== false ? [ent] : [],
|
|
1736
|
+
includeTest: options.includeTests !== false,
|
|
1737
|
+
projectRoot,
|
|
1738
|
+
dryRun: options.dryRun,
|
|
1739
|
+
overwrite: options.overwrite
|
|
1740
|
+
});
|
|
1741
|
+
files.push(...ucFiles);
|
|
1742
|
+
if (options.includeExpress !== false) {
|
|
1743
|
+
const httpFiles = generateHttpHandler({
|
|
1744
|
+
name: `${ucName}Http`,
|
|
1745
|
+
feature: featureName,
|
|
1746
|
+
routePrefix: routePath,
|
|
1747
|
+
method: "get",
|
|
1748
|
+
path: entitySubRoute ? `${entitySubRoute}/:${idParam}` : `/:${idParam}`,
|
|
1749
|
+
useCaseName: ucName,
|
|
1750
|
+
includeValidation: true,
|
|
1751
|
+
validationSource: "params",
|
|
1752
|
+
validationFields: [{ name: idParam, type: "string" }],
|
|
1753
|
+
projectRoot,
|
|
1754
|
+
dryRun: options.dryRun,
|
|
1755
|
+
overwrite: options.overwrite
|
|
1756
|
+
});
|
|
1757
|
+
files.push(...httpFiles);
|
|
1758
|
+
}
|
|
1759
|
+
} else if (op === "list") {
|
|
1760
|
+
const ucName = `List${pluralEntity}`;
|
|
1761
|
+
const ucFiles = generateUseCase({
|
|
1762
|
+
name: ucName,
|
|
1763
|
+
feature: featureName,
|
|
1764
|
+
targetDir: path7__default.default.join(targetDir, "useCases"),
|
|
1765
|
+
dependencies: deps,
|
|
1766
|
+
inputProperties: [
|
|
1767
|
+
{ name: "page", type: "number", optional: true },
|
|
1768
|
+
{ name: "limit", type: "number", optional: true }
|
|
1769
|
+
],
|
|
1770
|
+
outputProperties: [
|
|
1771
|
+
{ name: listVar, type: `${ent}[]` },
|
|
1772
|
+
{ name: "total", type: "number", optional: true },
|
|
1773
|
+
{ name: "page", type: "number", optional: true },
|
|
1774
|
+
{ name: "limit", type: "number", optional: true }
|
|
1775
|
+
],
|
|
1776
|
+
domainImports: options.includeDomain !== false ? [ent] : [],
|
|
1777
|
+
includeTest: options.includeTests !== false,
|
|
1778
|
+
projectRoot,
|
|
1779
|
+
dryRun: options.dryRun,
|
|
1780
|
+
overwrite: options.overwrite
|
|
1781
|
+
});
|
|
1782
|
+
files.push(...ucFiles);
|
|
1783
|
+
if (options.includeExpress !== false) {
|
|
1784
|
+
const httpFiles = generateHttpHandler({
|
|
1785
|
+
name: `${ucName}Http`,
|
|
1786
|
+
feature: featureName,
|
|
1787
|
+
routePrefix: routePath,
|
|
1788
|
+
method: "get",
|
|
1789
|
+
path: entitySubRoute || "/",
|
|
1790
|
+
useCaseName: ucName,
|
|
1791
|
+
includeValidation: true,
|
|
1792
|
+
validationSource: "query",
|
|
1793
|
+
validationFields: [
|
|
1794
|
+
{ name: "page", type: "number", optional: true },
|
|
1795
|
+
{ name: "limit", type: "number", optional: true }
|
|
1796
|
+
],
|
|
1797
|
+
projectRoot,
|
|
1798
|
+
dryRun: options.dryRun,
|
|
1799
|
+
overwrite: options.overwrite
|
|
1800
|
+
});
|
|
1801
|
+
files.push(...httpFiles);
|
|
1802
|
+
}
|
|
1803
|
+
} else if (op === "update") {
|
|
1804
|
+
const ucName = `Update${ent}`;
|
|
1805
|
+
const ucFiles = generateUseCase({
|
|
1806
|
+
name: ucName,
|
|
1807
|
+
feature: featureName,
|
|
1808
|
+
targetDir: path7__default.default.join(targetDir, "useCases"),
|
|
1809
|
+
dependencies: deps,
|
|
1810
|
+
inputProperties: [{ name: entVar, type: ent }],
|
|
1811
|
+
outputProperties: [{ name: entVar, type: ent }],
|
|
1812
|
+
domainImports: options.includeDomain !== false ? [ent] : [],
|
|
1813
|
+
includeTest: options.includeTests !== false,
|
|
1814
|
+
projectRoot,
|
|
1815
|
+
dryRun: options.dryRun,
|
|
1816
|
+
overwrite: options.overwrite
|
|
1817
|
+
});
|
|
1818
|
+
files.push(...ucFiles);
|
|
1819
|
+
if (options.includeExpress !== false) {
|
|
1820
|
+
const httpFiles = generateHttpHandler({
|
|
1821
|
+
name: `${ucName}Http`,
|
|
1822
|
+
feature: featureName,
|
|
1823
|
+
routePrefix: routePath,
|
|
1824
|
+
method: "put",
|
|
1825
|
+
path: entitySubRoute ? `${entitySubRoute}/:${idParam}` : `/:${idParam}`,
|
|
1826
|
+
useCaseName: ucName,
|
|
1827
|
+
includeValidation: true,
|
|
1828
|
+
validationSource: "body",
|
|
1829
|
+
validationFields: [{ name: entVar, type: "any" }],
|
|
1830
|
+
projectRoot,
|
|
1831
|
+
dryRun: options.dryRun,
|
|
1832
|
+
overwrite: options.overwrite
|
|
1833
|
+
});
|
|
1834
|
+
files.push(...httpFiles);
|
|
1835
|
+
}
|
|
1836
|
+
} else if (op === "delete") {
|
|
1837
|
+
const ucName = `Delete${ent}`;
|
|
1838
|
+
const ucFiles = generateUseCase({
|
|
1839
|
+
name: ucName,
|
|
1840
|
+
feature: featureName,
|
|
1841
|
+
targetDir: path7__default.default.join(targetDir, "useCases"),
|
|
1842
|
+
dependencies: deps,
|
|
1843
|
+
inputProperties: [{ name: idParam, type: "string" }],
|
|
1844
|
+
outputProperties: [{ name: "success", type: "boolean" }],
|
|
1845
|
+
includeTest: options.includeTests !== false,
|
|
1846
|
+
projectRoot,
|
|
1847
|
+
dryRun: options.dryRun,
|
|
1848
|
+
overwrite: options.overwrite
|
|
1849
|
+
});
|
|
1850
|
+
files.push(...ucFiles);
|
|
1851
|
+
if (options.includeExpress !== false) {
|
|
1852
|
+
const httpFiles = generateHttpHandler({
|
|
1853
|
+
name: `${ucName}Http`,
|
|
1854
|
+
feature: featureName,
|
|
1855
|
+
routePrefix: routePath,
|
|
1856
|
+
method: "delete",
|
|
1857
|
+
path: entitySubRoute ? `${entitySubRoute}/:${idParam}` : `/:${idParam}`,
|
|
1858
|
+
useCaseName: ucName,
|
|
1859
|
+
includeValidation: true,
|
|
1860
|
+
validationSource: "params",
|
|
1861
|
+
validationFields: [{ name: idParam, type: "string" }],
|
|
1862
|
+
projectRoot,
|
|
1863
|
+
dryRun: options.dryRun,
|
|
1864
|
+
overwrite: options.overwrite
|
|
1865
|
+
});
|
|
1866
|
+
files.push(...httpFiles);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (options.customUseCases && options.customUseCases.length > 0) {
|
|
1873
|
+
for (const customUc of options.customUseCases) {
|
|
1874
|
+
const ucName = toPascalCase(customUc.name);
|
|
1875
|
+
const ucFiles = generateUseCase({
|
|
1876
|
+
name: ucName,
|
|
1877
|
+
feature: featureName,
|
|
1878
|
+
targetDir: path7__default.default.join(targetDir, "useCases"),
|
|
1879
|
+
dependencies: customUc.dependencies || [],
|
|
1880
|
+
inputProperties: customUc.inputProperties || [],
|
|
1881
|
+
outputProperties: customUc.outputProperties || [],
|
|
1882
|
+
domainImports: customUc.domainImports || [],
|
|
1883
|
+
includeTest: customUc.includeTest !== false && options.includeTests !== false,
|
|
1884
|
+
projectRoot,
|
|
1885
|
+
dryRun: options.dryRun,
|
|
1886
|
+
overwrite: options.overwrite
|
|
1887
|
+
});
|
|
1888
|
+
files.push(...ucFiles);
|
|
1889
|
+
if (options.includeExpress !== false && customUc.createHttpHandler) {
|
|
1890
|
+
const handlerName = `${ucName}Http`;
|
|
1891
|
+
const httpFiles = generateHttpHandler({
|
|
1892
|
+
name: handlerName,
|
|
1893
|
+
feature: featureName,
|
|
1894
|
+
routePrefix: routePath,
|
|
1895
|
+
method: customUc.httpMethod || "post",
|
|
1896
|
+
path: customUc.httpPath || `/${toKebabCase(ucName)}`,
|
|
1897
|
+
useCaseName: ucName,
|
|
1898
|
+
includeValidation: customUc.inputProperties && customUc.inputProperties.length > 0 || false,
|
|
1899
|
+
validationSource: customUc.validationSource || (customUc.httpMethod === "get" ? "query" : "body"),
|
|
1900
|
+
validationFields: customUc.validationFields || customUc.inputProperties || [],
|
|
1901
|
+
projectRoot,
|
|
1902
|
+
dryRun: options.dryRun,
|
|
1903
|
+
overwrite: options.overwrite
|
|
1904
|
+
});
|
|
1905
|
+
files.push(...httpFiles);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (options.includeExpress !== false) {
|
|
1910
|
+
const pxDir = path7__default.default.join(targetDir, "pxExpress");
|
|
1911
|
+
const pxIndexPath = path7__default.default.join(pxDir, "index.ts");
|
|
1912
|
+
if (!files.some((f) => f.absolutePath === pxIndexPath)) {
|
|
1913
|
+
const pxIndexContent = renderTemplate(expressIndexTemplate, { routePath });
|
|
1914
|
+
files.push(
|
|
1915
|
+
writeFileSafely(pxIndexPath, pxIndexContent, {
|
|
1916
|
+
projectRoot,
|
|
1917
|
+
dryRun: options.dryRun,
|
|
1918
|
+
overwrite: false
|
|
1919
|
+
})
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
const uniqueFiles = [];
|
|
1924
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1925
|
+
for (const f of files) {
|
|
1926
|
+
if (!seen.has(f.absolutePath)) {
|
|
1927
|
+
seen.add(f.absolutePath);
|
|
1928
|
+
uniqueFiles.push(f);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return uniqueFiles;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// src/commands/feature.ts
|
|
1935
|
+
async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
1936
|
+
const projectRoot = findProjectRoot();
|
|
1937
|
+
let name = featureNameArg?.trim();
|
|
1938
|
+
if (!name && flags.yes) {
|
|
1939
|
+
name = "feature";
|
|
1940
|
+
}
|
|
1941
|
+
if (!name) {
|
|
1942
|
+
name = handleCancel(
|
|
1943
|
+
await p4__namespace.text({
|
|
1944
|
+
message: "What is the feature name?",
|
|
1945
|
+
placeholder: "e.g. users, auth, orders, billing",
|
|
1946
|
+
validate: (val) => {
|
|
1947
|
+
if (!val || !val.trim()) return "Feature name cannot be empty.";
|
|
1948
|
+
return void 0;
|
|
1949
|
+
}
|
|
1950
|
+
})
|
|
1951
|
+
).trim();
|
|
1952
|
+
}
|
|
1953
|
+
const normalizedName = toCamelCase(name);
|
|
1954
|
+
const defaultDir = path7__default.default.relative(projectRoot, path7__default.default.join(resolveFeaturesDir(projectRoot), normalizedName));
|
|
1955
|
+
let targetDirRel = defaultDir;
|
|
1956
|
+
if (!flags.yes && !flags.target) {
|
|
1957
|
+
targetDirRel = handleCancel(
|
|
1958
|
+
await p4__namespace.text({
|
|
1959
|
+
message: "Target directory for the feature:",
|
|
1960
|
+
initialValue: defaultDir,
|
|
1961
|
+
defaultValue: defaultDir
|
|
1962
|
+
})
|
|
1963
|
+
).trim();
|
|
1964
|
+
}
|
|
1965
|
+
const targetDir = flags.target ? path7__default.default.resolve(projectRoot, flags.target) : path7__default.default.resolve(projectRoot, targetDirRel);
|
|
1966
|
+
let includeExpress = flags.express !== void 0 ? Boolean(flags.express) : true;
|
|
1967
|
+
let routePath = `/${normalizedName}`;
|
|
1968
|
+
let includeDomain = true;
|
|
1969
|
+
let includeInterface = true;
|
|
1970
|
+
let includeInfra = true;
|
|
1971
|
+
let includeUseCase = true;
|
|
1972
|
+
let includeTests = true;
|
|
1973
|
+
let includeDiProvider = true;
|
|
1974
|
+
const defaultEntity = toPascalCase(toSingular(normalizedName));
|
|
1975
|
+
let entityNames = [defaultEntity];
|
|
1976
|
+
let crudOperations = ["create", "read", "list", "update", "delete"];
|
|
1977
|
+
const customUseCases = [];
|
|
1978
|
+
if (!flags.yes) {
|
|
1979
|
+
includeExpress = handleCancel(
|
|
1980
|
+
await p4__namespace.confirm({
|
|
1981
|
+
message: "Include Express Presentation layer (pxExpress routes & handlers)?",
|
|
1982
|
+
initialValue: true
|
|
1983
|
+
})
|
|
1984
|
+
);
|
|
1985
|
+
if (includeExpress) {
|
|
1986
|
+
routePath = handleCancel(
|
|
1987
|
+
await p4__namespace.text({
|
|
1988
|
+
message: "Express route prefix:",
|
|
1989
|
+
initialValue: `/${normalizedName}`,
|
|
1990
|
+
defaultValue: `/${normalizedName}`
|
|
1991
|
+
})
|
|
1992
|
+
).trim();
|
|
1993
|
+
}
|
|
1994
|
+
includeDomain = handleCancel(
|
|
1995
|
+
await p4__namespace.confirm({
|
|
1996
|
+
message: "Include Domain Entity/Entities?",
|
|
1997
|
+
initialValue: true
|
|
1998
|
+
})
|
|
1999
|
+
);
|
|
2000
|
+
if (includeDomain) {
|
|
2001
|
+
const domainInput = handleCancel(
|
|
2002
|
+
await p4__namespace.text({
|
|
2003
|
+
message: "Domain entity name(s) (comma-separated):",
|
|
2004
|
+
initialValue: defaultEntity,
|
|
2005
|
+
defaultValue: defaultEntity,
|
|
2006
|
+
placeholder: `e.g. ${defaultEntity}, ${defaultEntity}Item`,
|
|
2007
|
+
validate: (val) => {
|
|
2008
|
+
if (!val || !val.trim()) return "Please enter at least one domain entity name.";
|
|
2009
|
+
return void 0;
|
|
2010
|
+
}
|
|
2011
|
+
})
|
|
2012
|
+
).trim();
|
|
2013
|
+
entityNames = domainInput.split(",").map((s2) => toPascalCase(s2.trim())).filter(Boolean);
|
|
2014
|
+
if (entityNames.length === 0) {
|
|
2015
|
+
entityNames = [defaultEntity];
|
|
2016
|
+
}
|
|
2017
|
+
} else {
|
|
2018
|
+
entityNames = [defaultEntity];
|
|
2019
|
+
}
|
|
2020
|
+
includeInterface = handleCancel(
|
|
2021
|
+
await p4__namespace.confirm({
|
|
2022
|
+
message: `Include Domain Repository interface(s) for ${entityNames.join(", ")}?`,
|
|
2023
|
+
initialValue: true
|
|
2024
|
+
})
|
|
2025
|
+
);
|
|
2026
|
+
if (includeInterface) {
|
|
2027
|
+
includeInfra = handleCancel(
|
|
2028
|
+
await p4__namespace.confirm({
|
|
2029
|
+
message: `Include Infrastructure Stub implementation(s) for ${entityNames.join(", ")}?`,
|
|
2030
|
+
initialValue: true
|
|
2031
|
+
})
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
includeUseCase = handleCancel(
|
|
2035
|
+
await p4__namespace.confirm({
|
|
2036
|
+
message: "Include Domain Use Cases?",
|
|
2037
|
+
initialValue: true
|
|
2038
|
+
})
|
|
2039
|
+
);
|
|
2040
|
+
if (includeUseCase) {
|
|
2041
|
+
const crudChoices = [
|
|
2042
|
+
{ value: "create", label: "Create (Create<Entity>)", hint: "POST" },
|
|
2043
|
+
{ value: "read", label: "Read One (Get<Entity>)", hint: "GET /:id" },
|
|
2044
|
+
{ value: "list", label: "List with Pagination (List<Entity>s)", hint: "GET / (query: page, limit)" },
|
|
2045
|
+
{ value: "update", label: "Update (Update<Entity>)", hint: "PUT /:id" },
|
|
2046
|
+
{ value: "delete", label: "Delete (Delete<Entity>)", hint: "DELETE /:id" }
|
|
2047
|
+
];
|
|
2048
|
+
crudOperations = handleCancel(
|
|
2049
|
+
await p4__namespace.multiselect({
|
|
2050
|
+
message: "Select standard CRUD use cases to generate:",
|
|
2051
|
+
options: crudChoices,
|
|
2052
|
+
initialValues: ["create", "read", "list", "update", "delete"],
|
|
2053
|
+
required: false
|
|
2054
|
+
})
|
|
2055
|
+
);
|
|
2056
|
+
includeTests = handleCancel(
|
|
2057
|
+
await p4__namespace.confirm({
|
|
2058
|
+
message: "Generate Vitest unit tests for use cases?",
|
|
2059
|
+
initialValue: true
|
|
2060
|
+
})
|
|
2061
|
+
);
|
|
2062
|
+
const wantCustom = handleCancel(
|
|
2063
|
+
await p4__namespace.confirm({
|
|
2064
|
+
message: "Do you want to add any custom use cases to this feature?",
|
|
2065
|
+
initialValue: false
|
|
2066
|
+
})
|
|
2067
|
+
);
|
|
2068
|
+
if (wantCustom) {
|
|
2069
|
+
let addMore = true;
|
|
2070
|
+
const availableIfaces = entityNames.map((e) => `I${e}Repository`);
|
|
2071
|
+
while (addMore) {
|
|
2072
|
+
const customName = handleCancel(
|
|
2073
|
+
await p4__namespace.text({
|
|
2074
|
+
message: "Custom Use Case name:",
|
|
2075
|
+
placeholder: "e.g. ResetPassword, VerifyEmail, AssignRole",
|
|
2076
|
+
validate: (v) => v && v.trim() ? void 0 : "Use Case name cannot be empty"
|
|
2077
|
+
})
|
|
2078
|
+
).trim();
|
|
2079
|
+
const customUcName = toPascalCase(customName);
|
|
2080
|
+
let selectedDeps = [];
|
|
2081
|
+
if (availableIfaces.length > 0) {
|
|
2082
|
+
selectedDeps = handleCancel(
|
|
2083
|
+
await p4__namespace.multiselect({
|
|
2084
|
+
message: `Select domain interfaces to inject into ${customUcName}:`,
|
|
2085
|
+
options: availableIfaces.map((i) => ({ value: i, label: i })),
|
|
2086
|
+
required: false
|
|
2087
|
+
})
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
let customInputProps = [];
|
|
2091
|
+
const hasInput = handleCancel(
|
|
2092
|
+
await p4__namespace.confirm({
|
|
2093
|
+
message: `Does ${customUcName} accept input arguments?`,
|
|
2094
|
+
initialValue: true
|
|
2095
|
+
})
|
|
2096
|
+
);
|
|
2097
|
+
if (hasInput) {
|
|
2098
|
+
customInputProps = await promptProperties({
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
let customOutputProps = [];
|
|
2102
|
+
const hasCustomOutput = handleCancel(
|
|
2103
|
+
await p4__namespace.confirm({
|
|
2104
|
+
message: `Does ${customUcName} return custom output data?`,
|
|
2105
|
+
initialValue: false
|
|
2106
|
+
})
|
|
2107
|
+
);
|
|
2108
|
+
if (hasCustomOutput) {
|
|
2109
|
+
customOutputProps = await promptProperties({
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
const customIncludeTest = handleCancel(
|
|
2113
|
+
await p4__namespace.confirm({
|
|
2114
|
+
message: `Generate Vitest unit test for ${customUcName}?`,
|
|
2115
|
+
initialValue: true
|
|
2116
|
+
})
|
|
2117
|
+
);
|
|
2118
|
+
let customCreateHttp = false;
|
|
2119
|
+
let customHttpMethod = "post";
|
|
2120
|
+
let customHttpPath = `/${toKebabCase(customUcName)}`;
|
|
2121
|
+
if (includeExpress) {
|
|
2122
|
+
customCreateHttp = handleCancel(
|
|
2123
|
+
await p4__namespace.confirm({
|
|
2124
|
+
message: `Generate a matching ExpressRoute HTTP handler for ${customUcName}?`,
|
|
2125
|
+
initialValue: true
|
|
2126
|
+
})
|
|
2127
|
+
);
|
|
2128
|
+
if (customCreateHttp) {
|
|
2129
|
+
customHttpMethod = handleCancel(
|
|
2130
|
+
await p4__namespace.select({
|
|
2131
|
+
message: "HTTP method for route:",
|
|
2132
|
+
options: [
|
|
2133
|
+
{ value: "post", label: "POST" },
|
|
2134
|
+
{ value: "get", label: "GET" },
|
|
2135
|
+
{ value: "put", label: "PUT" },
|
|
2136
|
+
{ value: "patch", label: "PATCH" },
|
|
2137
|
+
{ value: "delete", label: "DELETE" }
|
|
2138
|
+
],
|
|
2139
|
+
initialValue: "post"
|
|
2140
|
+
})
|
|
2141
|
+
);
|
|
2142
|
+
customHttpPath = handleCancel(
|
|
2143
|
+
await p4__namespace.text({
|
|
2144
|
+
message: "Route subpath (relative to feature prefix):",
|
|
2145
|
+
initialValue: `/${toKebabCase(customUcName)}`,
|
|
2146
|
+
defaultValue: `/${toKebabCase(customUcName)}`
|
|
2147
|
+
})
|
|
2148
|
+
).trim();
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
customUseCases.push({
|
|
2152
|
+
name: customUcName,
|
|
2153
|
+
dependencies: selectedDeps,
|
|
2154
|
+
inputProperties: customInputProps,
|
|
2155
|
+
outputProperties: customOutputProps,
|
|
2156
|
+
includeTest: customIncludeTest,
|
|
2157
|
+
createHttpHandler: customCreateHttp,
|
|
2158
|
+
httpMethod: customHttpMethod,
|
|
2159
|
+
httpPath: customHttpPath,
|
|
2160
|
+
validationSource: customHttpMethod === "get" ? "query" : "body",
|
|
2161
|
+
validationFields: customInputProps
|
|
2162
|
+
});
|
|
2163
|
+
addMore = handleCancel(
|
|
2164
|
+
await p4__namespace.confirm({
|
|
2165
|
+
message: "Add another custom use case?",
|
|
2166
|
+
initialValue: false
|
|
2167
|
+
})
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
includeDiProvider = handleCancel(
|
|
2173
|
+
await p4__namespace.confirm({
|
|
2174
|
+
message: "Generate / update feature DI provider (diProvider.ts)?",
|
|
2175
|
+
initialValue: true
|
|
2176
|
+
})
|
|
2177
|
+
);
|
|
2178
|
+
} else {
|
|
2179
|
+
if (flags.domainNames) {
|
|
2180
|
+
entityNames = String(flags.domainNames).split(",").map((s2) => toPascalCase(s2.trim())).filter(Boolean);
|
|
2181
|
+
} else {
|
|
2182
|
+
entityNames = [defaultEntity];
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
const s = p4__namespace.spinner();
|
|
2186
|
+
s.start(`Generating feature '${pc13__default.default.cyan(normalizedName)}'...`);
|
|
2187
|
+
const options = {
|
|
2188
|
+
name: normalizedName,
|
|
2189
|
+
projectRoot,
|
|
2190
|
+
targetDir,
|
|
2191
|
+
dryRun: Boolean(flags.dryRun),
|
|
2192
|
+
overwrite: Boolean(flags.overwrite),
|
|
2193
|
+
includeExpress,
|
|
2194
|
+
routePath,
|
|
2195
|
+
includeDomain,
|
|
2196
|
+
entityNames,
|
|
2197
|
+
includeInterface,
|
|
2198
|
+
includeInfra,
|
|
2199
|
+
includeUseCase,
|
|
2200
|
+
crudOperations,
|
|
2201
|
+
customUseCases,
|
|
2202
|
+
includeTests,
|
|
2203
|
+
includeDiProvider
|
|
2204
|
+
};
|
|
2205
|
+
const files = generateFeature(options);
|
|
2206
|
+
s.stop("Feature scaffolded successfully!");
|
|
2207
|
+
printGeneratedFiles(files, options.dryRun);
|
|
2208
|
+
p4__namespace.outro(pc13__default.default.green(`Feature '${normalizedName}' ready to use!`));
|
|
2209
|
+
}
|
|
2210
|
+
async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
2211
|
+
const projectRoot = findProjectRoot();
|
|
2212
|
+
const existingFeatures = listExistingFeatures(projectRoot);
|
|
2213
|
+
let feature = flags.feature?.trim();
|
|
2214
|
+
if (!feature) {
|
|
2215
|
+
if (existingFeatures.length > 0) {
|
|
2216
|
+
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2217
|
+
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2218
|
+
const selected = handleCancel(
|
|
2219
|
+
await p4__namespace.select({
|
|
2220
|
+
message: "Select target feature:",
|
|
2221
|
+
options: featureOptions
|
|
2222
|
+
})
|
|
2223
|
+
);
|
|
2224
|
+
if (selected === "__custom__") {
|
|
2225
|
+
feature = handleCancel(
|
|
2226
|
+
await p4__namespace.text({
|
|
2227
|
+
message: "Enter feature name:",
|
|
2228
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2229
|
+
})
|
|
2230
|
+
).trim();
|
|
2231
|
+
} else {
|
|
2232
|
+
feature = selected;
|
|
2233
|
+
}
|
|
2234
|
+
} else {
|
|
2235
|
+
feature = handleCancel(
|
|
2236
|
+
await p4__namespace.text({
|
|
2237
|
+
message: "Enter target feature name:",
|
|
2238
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2239
|
+
})
|
|
2240
|
+
).trim();
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
feature = toCamelCase(feature);
|
|
2244
|
+
let name = useCaseNameArg?.trim();
|
|
2245
|
+
if (!name) {
|
|
2246
|
+
name = handleCancel(
|
|
2247
|
+
await p4__namespace.text({
|
|
2248
|
+
message: "Use Case name:",
|
|
2249
|
+
placeholder: "e.g. CreateUser, GetUser, CancelOrder",
|
|
2250
|
+
validate: (v) => v && v.trim() ? void 0 : "Use Case name cannot be empty"
|
|
2251
|
+
})
|
|
2252
|
+
).trim();
|
|
2253
|
+
}
|
|
2254
|
+
const ucName = toPascalCase(name);
|
|
2255
|
+
const availableInterfaces = listDomainInterfaces(projectRoot, feature);
|
|
2256
|
+
let selectedDeps = [];
|
|
2257
|
+
if (!flags.yes && availableInterfaces.length > 0) {
|
|
2258
|
+
const multiOptions = availableInterfaces.map((i) => ({
|
|
2259
|
+
value: i.name,
|
|
2260
|
+
label: i.name
|
|
2261
|
+
}));
|
|
2262
|
+
selectedDeps = handleCancel(
|
|
2263
|
+
await p4__namespace.multiselect({
|
|
2264
|
+
message: "Select domain interfaces to inject into use case:",
|
|
2265
|
+
options: multiOptions,
|
|
2266
|
+
required: false
|
|
2267
|
+
})
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
let inputProps = [];
|
|
2271
|
+
let outputProps = [];
|
|
2272
|
+
let includeTest = true;
|
|
2273
|
+
let createHttp = false;
|
|
2274
|
+
let httpMethod = "post";
|
|
2275
|
+
let httpPath = "/";
|
|
2276
|
+
if (!flags.yes) {
|
|
2277
|
+
const hasInput = handleCancel(
|
|
2278
|
+
await p4__namespace.confirm({
|
|
2279
|
+
message: "Does this use case accept input arguments?",
|
|
2280
|
+
initialValue: true
|
|
2281
|
+
})
|
|
2282
|
+
);
|
|
2283
|
+
if (hasInput) {
|
|
2284
|
+
inputProps = await promptProperties({
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
const hasCustomOutput = handleCancel(
|
|
2288
|
+
await p4__namespace.confirm({
|
|
2289
|
+
message: "Does this use case return custom output data?",
|
|
2290
|
+
initialValue: false
|
|
2291
|
+
})
|
|
2292
|
+
);
|
|
2293
|
+
if (hasCustomOutput) {
|
|
2294
|
+
outputProps = await promptProperties({
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
includeTest = handleCancel(
|
|
2298
|
+
await p4__namespace.confirm({
|
|
2299
|
+
message: "Generate Vitest unit test skeleton (.test.ts)?",
|
|
2300
|
+
initialValue: true
|
|
2301
|
+
})
|
|
2302
|
+
);
|
|
2303
|
+
createHttp = handleCancel(
|
|
2304
|
+
await p4__namespace.confirm({
|
|
2305
|
+
message: "Generate a matching ExpressRoute HTTP handler?",
|
|
2306
|
+
initialValue: false
|
|
2307
|
+
})
|
|
2308
|
+
);
|
|
2309
|
+
if (createHttp) {
|
|
2310
|
+
httpMethod = handleCancel(
|
|
2311
|
+
await p4__namespace.select({
|
|
2312
|
+
message: "HTTP method for route:",
|
|
2313
|
+
options: [
|
|
2314
|
+
{ value: "post", label: "POST" },
|
|
2315
|
+
{ value: "get", label: "GET" },
|
|
2316
|
+
{ value: "put", label: "PUT" },
|
|
2317
|
+
{ value: "patch", label: "PATCH" },
|
|
2318
|
+
{ value: "delete", label: "DELETE" }
|
|
2319
|
+
],
|
|
2320
|
+
initialValue: "post"
|
|
2321
|
+
})
|
|
2322
|
+
);
|
|
2323
|
+
httpPath = handleCancel(
|
|
2324
|
+
await p4__namespace.text({
|
|
2325
|
+
message: "Route subpath (relative to feature prefix):",
|
|
2326
|
+
initialValue: "/",
|
|
2327
|
+
defaultValue: "/"
|
|
2328
|
+
})
|
|
2329
|
+
).trim();
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
const s = p4__namespace.spinner();
|
|
2333
|
+
s.start(`Generating use case '${pc13__default.default.cyan(ucName)}'...`);
|
|
2334
|
+
const options = {
|
|
2335
|
+
name: ucName,
|
|
2336
|
+
feature,
|
|
2337
|
+
dependencies: selectedDeps,
|
|
2338
|
+
inputProperties: inputProps,
|
|
2339
|
+
outputProperties: outputProps,
|
|
2340
|
+
includeTest,
|
|
2341
|
+
projectRoot,
|
|
2342
|
+
dryRun: Boolean(flags.dryRun),
|
|
2343
|
+
overwrite: Boolean(flags.overwrite)
|
|
2344
|
+
};
|
|
2345
|
+
const files = generateUseCase(options);
|
|
2346
|
+
if (createHttp) {
|
|
2347
|
+
const handlerName = `${ucName}Http`;
|
|
2348
|
+
const httpFiles = generateHttpHandler({
|
|
2349
|
+
name: handlerName,
|
|
2350
|
+
feature,
|
|
2351
|
+
method: httpMethod,
|
|
2352
|
+
path: httpPath,
|
|
2353
|
+
useCaseName: ucName,
|
|
2354
|
+
includeValidation: inputProps.length > 0,
|
|
2355
|
+
validationSource: httpMethod === "get" ? "query" : "body",
|
|
2356
|
+
validationFields: inputProps,
|
|
2357
|
+
projectRoot,
|
|
2358
|
+
dryRun: Boolean(flags.dryRun),
|
|
2359
|
+
overwrite: Boolean(flags.overwrite)
|
|
2360
|
+
});
|
|
2361
|
+
files.push(...httpFiles);
|
|
2362
|
+
}
|
|
2363
|
+
s.stop("Use case generated successfully!");
|
|
2364
|
+
printGeneratedFiles(files, options.dryRun);
|
|
2365
|
+
p4__namespace.outro(pc13__default.default.green(`Use case '${ucName}' created in feature '${feature}'!`));
|
|
2366
|
+
}
|
|
2367
|
+
async function runDomainCommand(domainNameArg, flags = {}) {
|
|
2368
|
+
const projectRoot = findProjectRoot();
|
|
2369
|
+
const existingFeatures = listExistingFeatures(projectRoot);
|
|
2370
|
+
let feature = flags.feature?.trim();
|
|
2371
|
+
if (!feature) {
|
|
2372
|
+
if (existingFeatures.length > 0) {
|
|
2373
|
+
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2374
|
+
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2375
|
+
const selected = handleCancel(
|
|
2376
|
+
await p4__namespace.select({
|
|
2377
|
+
message: "Select target feature:",
|
|
2378
|
+
options: featureOptions
|
|
2379
|
+
})
|
|
2380
|
+
);
|
|
2381
|
+
if (selected === "__custom__") {
|
|
2382
|
+
feature = handleCancel(
|
|
2383
|
+
await p4__namespace.text({
|
|
2384
|
+
message: "Enter feature name:",
|
|
2385
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2386
|
+
})
|
|
2387
|
+
).trim();
|
|
2388
|
+
} else {
|
|
2389
|
+
feature = selected;
|
|
2390
|
+
}
|
|
2391
|
+
} else {
|
|
2392
|
+
feature = handleCancel(
|
|
2393
|
+
await p4__namespace.text({
|
|
2394
|
+
message: "Enter target feature name:",
|
|
2395
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2396
|
+
})
|
|
2397
|
+
).trim();
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
feature = toCamelCase(feature);
|
|
2401
|
+
let name = domainNameArg?.trim();
|
|
2402
|
+
if (!name) {
|
|
2403
|
+
name = handleCancel(
|
|
2404
|
+
await p4__namespace.text({
|
|
2405
|
+
message: "Domain model / entity name:",
|
|
2406
|
+
placeholder: "e.g. User, Order, Product, Address",
|
|
2407
|
+
validate: (v) => v && v.trim() ? void 0 : "Domain name cannot be empty"
|
|
2408
|
+
})
|
|
2409
|
+
).trim();
|
|
2410
|
+
}
|
|
2411
|
+
const entityName = toPascalCase(name);
|
|
2412
|
+
let kind = "interface";
|
|
2413
|
+
let properties = [];
|
|
2414
|
+
if (!flags.yes) {
|
|
2415
|
+
kind = handleCancel(
|
|
2416
|
+
await p4__namespace.select({
|
|
2417
|
+
message: "Select domain model type:",
|
|
2418
|
+
options: [
|
|
2419
|
+
{ value: "interface", label: "TypeScript Interface (pure data structure)" },
|
|
2420
|
+
{ value: "class", label: "Entity Class (with constructor & properties)" },
|
|
2421
|
+
{ value: "valueObject", label: "Value Object (immutable domain object with equals)" }
|
|
2422
|
+
],
|
|
2423
|
+
initialValue: "interface"
|
|
2424
|
+
})
|
|
2425
|
+
);
|
|
2426
|
+
const defaultProps = [
|
|
2427
|
+
{ name: "id", type: "string" },
|
|
2428
|
+
{ name: "createdAt", type: "Date" },
|
|
2429
|
+
{ name: "updatedAt", type: "Date" }
|
|
2430
|
+
];
|
|
2431
|
+
properties = await promptProperties({
|
|
2432
|
+
defaults: kind === "valueObject" ? [{ name: "value", type: "string" }] : defaultProps
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
const s = p4__namespace.spinner();
|
|
2436
|
+
s.start(`Generating domain model '${pc13__default.default.cyan(entityName)}'...`);
|
|
2437
|
+
const options = {
|
|
2438
|
+
name: entityName,
|
|
2439
|
+
feature,
|
|
2440
|
+
kind,
|
|
2441
|
+
properties,
|
|
2442
|
+
projectRoot,
|
|
2443
|
+
dryRun: Boolean(flags.dryRun),
|
|
2444
|
+
overwrite: Boolean(flags.overwrite)
|
|
2445
|
+
};
|
|
2446
|
+
const file = generateDomain(options);
|
|
2447
|
+
s.stop("Domain model generated successfully!");
|
|
2448
|
+
printGeneratedFiles([file], options.dryRun);
|
|
2449
|
+
p4__namespace.outro(pc13__default.default.green(`Domain model '${entityName}' created in feature '${feature}'!`));
|
|
2450
|
+
}
|
|
2451
|
+
async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
2452
|
+
const projectRoot = findProjectRoot();
|
|
2453
|
+
const existingFeatures = listExistingFeatures(projectRoot);
|
|
2454
|
+
let scope = flags.global ? "global" : "feature";
|
|
2455
|
+
let feature = flags.feature?.trim();
|
|
2456
|
+
if (!flags.yes && !flags.global && !flags.feature) {
|
|
2457
|
+
scope = handleCancel(
|
|
2458
|
+
await p4__namespace.select({
|
|
2459
|
+
message: "Where should this infrastructure implementation be placed?",
|
|
2460
|
+
options: [
|
|
2461
|
+
{ value: "feature", label: "Feature-level (features/<feature>/infrastructure)" },
|
|
2462
|
+
{ value: "global", label: "Global / Core (infrastructure/)" }
|
|
2463
|
+
],
|
|
2464
|
+
initialValue: "feature"
|
|
2465
|
+
})
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
if (scope === "feature" && !feature) {
|
|
2469
|
+
if (existingFeatures.length > 0) {
|
|
2470
|
+
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2471
|
+
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2472
|
+
const selected = handleCancel(
|
|
2473
|
+
await p4__namespace.select({
|
|
2474
|
+
message: "Select target feature:",
|
|
2475
|
+
options: featureOptions
|
|
2476
|
+
})
|
|
2477
|
+
);
|
|
2478
|
+
if (selected === "__custom__") {
|
|
2479
|
+
feature = handleCancel(
|
|
2480
|
+
await p4__namespace.text({
|
|
2481
|
+
message: "Enter feature name:",
|
|
2482
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2483
|
+
})
|
|
2484
|
+
).trim();
|
|
2485
|
+
} else {
|
|
2486
|
+
feature = selected;
|
|
2487
|
+
}
|
|
2488
|
+
} else {
|
|
2489
|
+
feature = handleCancel(
|
|
2490
|
+
await p4__namespace.text({
|
|
2491
|
+
message: "Enter target feature name:",
|
|
2492
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2493
|
+
})
|
|
2494
|
+
).trim();
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
if (feature) {
|
|
2498
|
+
feature = toCamelCase(feature);
|
|
2499
|
+
}
|
|
2500
|
+
let name = classNameArg?.trim();
|
|
2501
|
+
if (!name) {
|
|
2502
|
+
name = handleCancel(
|
|
2503
|
+
await p4__namespace.text({
|
|
2504
|
+
message: "Infrastructure class name:",
|
|
2505
|
+
placeholder: "e.g. StubUserRepository, PostgresUserRepository, S3FileStorage",
|
|
2506
|
+
validate: (v) => v && v.trim() ? void 0 : "Class name cannot be empty"
|
|
2507
|
+
})
|
|
2508
|
+
).trim();
|
|
2509
|
+
}
|
|
2510
|
+
const className = toPascalCase(name);
|
|
2511
|
+
let kind = flags.kind ? flags.kind === "stub" ? "stub" : "concrete" : className.startsWith("Stub") ? "stub" : "concrete";
|
|
2512
|
+
let implementsInterface = flags.interface?.trim() || void 0;
|
|
2513
|
+
let registerInProvider = false;
|
|
2514
|
+
if (scope === "feature" && feature && !implementsInterface) {
|
|
2515
|
+
const interfaces = listDomainInterfaces(projectRoot, feature);
|
|
2516
|
+
if (!flags.yes) {
|
|
2517
|
+
if (interfaces.length > 0) {
|
|
2518
|
+
const match = interfaces.find(
|
|
2519
|
+
(i) => className.toLowerCase().includes(removeInterfacePrefix(i.name).toLowerCase())
|
|
2520
|
+
);
|
|
2521
|
+
const ifaceChoice = handleCancel(
|
|
2522
|
+
await p4__namespace.select({
|
|
2523
|
+
message: "Does this class implement a domain interface or abstract class?",
|
|
2524
|
+
options: [
|
|
2525
|
+
...interfaces.map((i) => ({ value: i.name, label: i.name })),
|
|
2526
|
+
{ value: "__custom__", label: "\u270F\uFE0F Enter custom interface or abstract class..." },
|
|
2527
|
+
{ value: "__none__", label: "None / Standalone" }
|
|
2528
|
+
],
|
|
2529
|
+
initialValue: match?.name || interfaces[0]?.name
|
|
2530
|
+
})
|
|
2531
|
+
);
|
|
2532
|
+
if (ifaceChoice === "__custom__") {
|
|
2533
|
+
implementsInterface = handleCancel(
|
|
2534
|
+
await p4__namespace.text({
|
|
2535
|
+
message: "Enter interface or abstract class name:",
|
|
2536
|
+
placeholder: "e.g. IUserRepository, BaseStorageAdapter",
|
|
2537
|
+
validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
|
|
2538
|
+
})
|
|
2539
|
+
).trim();
|
|
2540
|
+
} else if (ifaceChoice !== "__none__") {
|
|
2541
|
+
implementsInterface = ifaceChoice;
|
|
2542
|
+
}
|
|
2543
|
+
} else {
|
|
2544
|
+
const wantImpl = handleCancel(
|
|
2545
|
+
await p4__namespace.confirm({
|
|
2546
|
+
message: "Does this class implement a domain interface or abstract class?",
|
|
2547
|
+
initialValue: false
|
|
2548
|
+
})
|
|
2549
|
+
);
|
|
2550
|
+
if (wantImpl) {
|
|
2551
|
+
implementsInterface = handleCancel(
|
|
2552
|
+
await p4__namespace.text({
|
|
2553
|
+
message: "Enter interface or abstract class name:",
|
|
2554
|
+
placeholder: "e.g. IUserRepository, BaseStorageAdapter",
|
|
2555
|
+
validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
|
|
2556
|
+
})
|
|
2557
|
+
).trim();
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
} else if (flags.yes && interfaces.length > 0) {
|
|
2561
|
+
const match = interfaces.find(
|
|
2562
|
+
(i) => className.toLowerCase().includes(removeInterfacePrefix(i.name).toLowerCase())
|
|
2563
|
+
);
|
|
2564
|
+
if (match) {
|
|
2565
|
+
implementsInterface = match.name;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
} else if (scope === "global" && !implementsInterface && !flags.yes) {
|
|
2569
|
+
const allInterfaces = listDomainInterfaces(projectRoot);
|
|
2570
|
+
if (allInterfaces.length > 0) {
|
|
2571
|
+
const ifaceChoice = handleCancel(
|
|
2572
|
+
await p4__namespace.select({
|
|
2573
|
+
message: "Does this class implement an interface or abstract class?",
|
|
2574
|
+
options: [
|
|
2575
|
+
...allInterfaces.map((i) => ({ value: i.name, label: `${i.name} (${i.feature})` })),
|
|
2576
|
+
{ value: "__custom__", label: "\u270F\uFE0F Enter custom interface or abstract class..." },
|
|
2577
|
+
{ value: "__none__", label: "None / Standalone" }
|
|
2578
|
+
],
|
|
2579
|
+
initialValue: "__none__"
|
|
2580
|
+
})
|
|
2581
|
+
);
|
|
2582
|
+
if (ifaceChoice === "__custom__") {
|
|
2583
|
+
implementsInterface = handleCancel(
|
|
2584
|
+
await p4__namespace.text({
|
|
2585
|
+
message: "Enter interface or abstract class name:",
|
|
2586
|
+
placeholder: "e.g. IAuthRepository",
|
|
2587
|
+
validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
|
|
2588
|
+
})
|
|
2589
|
+
).trim();
|
|
2590
|
+
} else if (ifaceChoice !== "__none__") {
|
|
2591
|
+
implementsInterface = ifaceChoice;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
if (!flags.yes && !flags.kind) {
|
|
2596
|
+
kind = handleCancel(
|
|
2597
|
+
await p4__namespace.select({
|
|
2598
|
+
message: "Select implementation kind:",
|
|
2599
|
+
options: [
|
|
2600
|
+
{ value: "stub", label: "In-Memory Stub (with in-memory store and test helper methods)" },
|
|
2601
|
+
{ value: "concrete", label: "Concrete Adapter / Service Skeleton (@MakeInjectable)" }
|
|
2602
|
+
],
|
|
2603
|
+
initialValue: kind
|
|
2604
|
+
})
|
|
2605
|
+
);
|
|
2606
|
+
if (scope === "feature" && feature && implementsInterface) {
|
|
2607
|
+
registerInProvider = handleCancel(
|
|
2608
|
+
await p4__namespace.confirm({
|
|
2609
|
+
message: "Register this implementation in feature diProvider.ts?",
|
|
2610
|
+
initialValue: true
|
|
2611
|
+
})
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
} else if (flags.yes && scope === "feature" && feature && implementsInterface) {
|
|
2615
|
+
registerInProvider = !flags.noProvider;
|
|
2616
|
+
}
|
|
2617
|
+
const s = p4__namespace.spinner();
|
|
2618
|
+
s.start(`Generating infrastructure class '${pc13__default.default.cyan(className)}'...`);
|
|
2619
|
+
const files = [];
|
|
2620
|
+
const options = {
|
|
2621
|
+
name: className,
|
|
2622
|
+
feature,
|
|
2623
|
+
scope,
|
|
2624
|
+
kind,
|
|
2625
|
+
implementsInterface,
|
|
2626
|
+
projectRoot,
|
|
2627
|
+
dryRun: Boolean(flags.dryRun),
|
|
2628
|
+
overwrite: Boolean(flags.overwrite)
|
|
2629
|
+
};
|
|
2630
|
+
const infraFile = generateInfrastructure(options);
|
|
2631
|
+
files.push(infraFile);
|
|
2632
|
+
if (registerInProvider && scope === "feature" && feature && implementsInterface) {
|
|
2633
|
+
const featureDir = path7__default.default.join(resolveFeaturesDir(projectRoot), feature);
|
|
2634
|
+
const providerFile = updateOrGenerateDiProvider(
|
|
2635
|
+
featureDir,
|
|
2636
|
+
feature,
|
|
2637
|
+
[
|
|
2638
|
+
{
|
|
2639
|
+
ifaceName: implementsInterface,
|
|
2640
|
+
implName: className
|
|
2641
|
+
}
|
|
2642
|
+
],
|
|
2643
|
+
{
|
|
2644
|
+
projectRoot,
|
|
2645
|
+
dryRun: Boolean(flags.dryRun),
|
|
2646
|
+
overwrite: Boolean(flags.overwrite)
|
|
2647
|
+
}
|
|
2648
|
+
);
|
|
2649
|
+
files.push(providerFile);
|
|
2650
|
+
}
|
|
2651
|
+
s.stop("Infrastructure class generated successfully!");
|
|
2652
|
+
printGeneratedFiles(files, options.dryRun);
|
|
2653
|
+
p4__namespace.outro(pc13__default.default.green(`Infrastructure class '${className}' created!`));
|
|
2654
|
+
}
|
|
2655
|
+
async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
2656
|
+
const projectRoot = findProjectRoot();
|
|
2657
|
+
const existingFeatures = listExistingFeatures(projectRoot);
|
|
2658
|
+
let feature = flags.feature?.trim();
|
|
2659
|
+
if (!feature) {
|
|
2660
|
+
if (existingFeatures.length > 0) {
|
|
2661
|
+
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2662
|
+
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2663
|
+
const selected = handleCancel(
|
|
2664
|
+
await p4__namespace.select({
|
|
2665
|
+
message: "Select target feature:",
|
|
2666
|
+
options: featureOptions
|
|
2667
|
+
})
|
|
2668
|
+
);
|
|
2669
|
+
if (selected === "__custom__") {
|
|
2670
|
+
feature = handleCancel(
|
|
2671
|
+
await p4__namespace.text({
|
|
2672
|
+
message: "Enter feature name:",
|
|
2673
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2674
|
+
})
|
|
2675
|
+
).trim();
|
|
2676
|
+
} else {
|
|
2677
|
+
feature = selected;
|
|
2678
|
+
}
|
|
2679
|
+
} else {
|
|
2680
|
+
feature = handleCancel(
|
|
2681
|
+
await p4__namespace.text({
|
|
2682
|
+
message: "Enter target feature name:",
|
|
2683
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2684
|
+
})
|
|
2685
|
+
).trim();
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
feature = toCamelCase(feature);
|
|
2689
|
+
let name = interfaceNameArg?.trim();
|
|
2690
|
+
if (!name) {
|
|
2691
|
+
name = handleCancel(
|
|
2692
|
+
await p4__namespace.text({
|
|
2693
|
+
message: "Domain interface name:",
|
|
2694
|
+
placeholder: "e.g. IUserRepository, IPaymentService",
|
|
2695
|
+
validate: (v) => v && v.trim() ? void 0 : "Interface name cannot be empty"
|
|
2696
|
+
})
|
|
2697
|
+
).trim();
|
|
2698
|
+
}
|
|
2699
|
+
const ifaceName = ensureInterfacePrefix(name);
|
|
2700
|
+
const existingEntities = listDomainEntities(projectRoot, feature);
|
|
2701
|
+
const fallbackEntity = existingEntities.length > 0 ? existingEntities.find((e) => ifaceName.toLowerCase().includes(e.name.toLowerCase()))?.name || existingEntities[0]?.name : removeInterfacePrefix(ifaceName).replace(/Repository$|Service$/, "");
|
|
2702
|
+
let entityName = flags.entity || fallbackEntity;
|
|
2703
|
+
let kind = "abstractClass";
|
|
2704
|
+
let methodsPreset = "crud";
|
|
2705
|
+
let generateStub = true;
|
|
2706
|
+
let registerInProvider = true;
|
|
2707
|
+
if (!flags.yes) {
|
|
2708
|
+
if (existingEntities.length > 0) {
|
|
2709
|
+
const selectedEntity = handleCancel(
|
|
2710
|
+
await p4__namespace.select({
|
|
2711
|
+
message: "Select domain entity associated with this interface:",
|
|
2712
|
+
options: [
|
|
2713
|
+
...existingEntities.map((e) => ({ value: e.name, label: e.name })),
|
|
2714
|
+
{ value: "__custom__", label: "\u270F\uFE0F Enter custom entity name..." }
|
|
2715
|
+
],
|
|
2716
|
+
initialValue: fallbackEntity
|
|
2717
|
+
})
|
|
2718
|
+
);
|
|
2719
|
+
if (selectedEntity === "__custom__") {
|
|
2720
|
+
entityName = handleCancel(
|
|
2721
|
+
await p4__namespace.text({
|
|
2722
|
+
message: "Enter entity name:",
|
|
2723
|
+
defaultValue: removeInterfacePrefix(ifaceName).replace(/Repository$|Service$/, "")
|
|
2724
|
+
})
|
|
2725
|
+
).trim();
|
|
2726
|
+
} else {
|
|
2727
|
+
entityName = selectedEntity;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
kind = handleCancel(
|
|
2731
|
+
await p4__namespace.select({
|
|
2732
|
+
message: "Interface definition style:",
|
|
2733
|
+
options: [
|
|
2734
|
+
{ value: "abstractClass", label: "Abstract Class (Recommended - acts as DI token & type)" },
|
|
2735
|
+
{ value: "interface", label: "TypeScript Interface (type-only)" }
|
|
2736
|
+
],
|
|
2737
|
+
initialValue: "abstractClass"
|
|
2738
|
+
})
|
|
2739
|
+
);
|
|
2740
|
+
methodsPreset = handleCancel(
|
|
2741
|
+
await p4__namespace.select({
|
|
2742
|
+
message: "Method signature preset:",
|
|
2743
|
+
options: [
|
|
2744
|
+
{ value: "crud", label: "Standard CRUD (GetById, List, Create, Update, Delete)" },
|
|
2745
|
+
{ value: "empty", label: "Empty / Minimal skeleton" }
|
|
2746
|
+
],
|
|
2747
|
+
initialValue: "crud"
|
|
2748
|
+
})
|
|
2749
|
+
);
|
|
2750
|
+
generateStub = handleCancel(
|
|
2751
|
+
await p4__namespace.confirm({
|
|
2752
|
+
message: "Auto-generate Infrastructure Stub implementation?",
|
|
2753
|
+
initialValue: true
|
|
2754
|
+
})
|
|
2755
|
+
);
|
|
2756
|
+
if (generateStub) {
|
|
2757
|
+
registerInProvider = handleCancel(
|
|
2758
|
+
await p4__namespace.confirm({
|
|
2759
|
+
message: "Auto-register interface & stub in feature diProvider.ts?",
|
|
2760
|
+
initialValue: true
|
|
2761
|
+
})
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
const s = p4__namespace.spinner();
|
|
2766
|
+
s.start(`Generating interface '${pc13__default.default.cyan(ifaceName)}'...`);
|
|
2767
|
+
const files = [];
|
|
2768
|
+
const options = {
|
|
2769
|
+
name: ifaceName,
|
|
2770
|
+
feature,
|
|
2771
|
+
kind,
|
|
2772
|
+
entityName,
|
|
2773
|
+
methodsPreset,
|
|
2774
|
+
projectRoot,
|
|
2775
|
+
dryRun: Boolean(flags.dryRun),
|
|
2776
|
+
overwrite: Boolean(flags.overwrite)
|
|
2777
|
+
};
|
|
2778
|
+
const ifaceFile = generateInterface(options);
|
|
2779
|
+
files.push(ifaceFile);
|
|
2780
|
+
const stubClassName = toStubName(ifaceName);
|
|
2781
|
+
if (generateStub) {
|
|
2782
|
+
const stubFile = generateInfrastructure({
|
|
2783
|
+
name: stubClassName,
|
|
2784
|
+
feature,
|
|
2785
|
+
kind: "stub",
|
|
2786
|
+
implementsInterface: ifaceName,
|
|
2787
|
+
entityName,
|
|
2788
|
+
projectRoot,
|
|
2789
|
+
dryRun: Boolean(flags.dryRun),
|
|
2790
|
+
overwrite: Boolean(flags.overwrite)
|
|
2791
|
+
});
|
|
2792
|
+
files.push(stubFile);
|
|
2793
|
+
}
|
|
2794
|
+
if (registerInProvider && generateStub) {
|
|
2795
|
+
const featureDir = path7__default.default.join(resolveFeaturesDir(projectRoot), feature);
|
|
2796
|
+
const providerFile = updateOrGenerateDiProvider(
|
|
2797
|
+
featureDir,
|
|
2798
|
+
feature,
|
|
2799
|
+
[
|
|
2800
|
+
{
|
|
2801
|
+
ifaceName,
|
|
2802
|
+
implName: stubClassName
|
|
2803
|
+
}
|
|
2804
|
+
],
|
|
2805
|
+
{
|
|
2806
|
+
projectRoot,
|
|
2807
|
+
dryRun: Boolean(flags.dryRun),
|
|
2808
|
+
overwrite: Boolean(flags.overwrite)
|
|
2809
|
+
}
|
|
2810
|
+
);
|
|
2811
|
+
files.push(providerFile);
|
|
2812
|
+
}
|
|
2813
|
+
s.stop("Interface generated successfully!");
|
|
2814
|
+
printGeneratedFiles(files, options.dryRun);
|
|
2815
|
+
p4__namespace.outro(pc13__default.default.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
|
|
2816
|
+
}
|
|
2817
|
+
async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
2818
|
+
const projectRoot = findProjectRoot();
|
|
2819
|
+
const existingFeatures = listExistingFeatures(projectRoot);
|
|
2820
|
+
let feature = flags.feature?.trim();
|
|
2821
|
+
if (!feature) {
|
|
2822
|
+
if (existingFeatures.length > 0) {
|
|
2823
|
+
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2824
|
+
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2825
|
+
const selected = handleCancel(
|
|
2826
|
+
await p4__namespace.select({
|
|
2827
|
+
message: "Select target feature:",
|
|
2828
|
+
options: featureOptions
|
|
2829
|
+
})
|
|
2830
|
+
);
|
|
2831
|
+
if (selected === "__custom__") {
|
|
2832
|
+
feature = handleCancel(
|
|
2833
|
+
await p4__namespace.text({
|
|
2834
|
+
message: "Enter feature name:",
|
|
2835
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2836
|
+
})
|
|
2837
|
+
).trim();
|
|
2838
|
+
} else {
|
|
2839
|
+
feature = selected;
|
|
2840
|
+
}
|
|
2841
|
+
} else {
|
|
2842
|
+
feature = handleCancel(
|
|
2843
|
+
await p4__namespace.text({
|
|
2844
|
+
message: "Enter target feature name:",
|
|
2845
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2846
|
+
})
|
|
2847
|
+
).trim();
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
feature = toCamelCase(feature);
|
|
2851
|
+
let name = handlerNameArg?.trim();
|
|
2852
|
+
if (!name) {
|
|
2853
|
+
name = handleCancel(
|
|
2854
|
+
await p4__namespace.text({
|
|
2855
|
+
message: "HTTP Route Handler class name:",
|
|
2856
|
+
placeholder: "e.g. CreateUserHttp, GetUserHttp, HealthHttp",
|
|
2857
|
+
validate: (v) => v && v.trim() ? void 0 : "Handler name cannot be empty"
|
|
2858
|
+
})
|
|
2859
|
+
).trim();
|
|
2860
|
+
}
|
|
2861
|
+
let className = toPascalCase(name);
|
|
2862
|
+
if (!className.endsWith("Http") && !className.endsWith("Route")) {
|
|
2863
|
+
className = `${className}Http`;
|
|
2864
|
+
}
|
|
2865
|
+
let method = flags.method || "get";
|
|
2866
|
+
let subPath = flags.path || "/";
|
|
2867
|
+
let useCaseName = flags.usecase;
|
|
2868
|
+
let includeValidation = true;
|
|
2869
|
+
let validationSource = "body";
|
|
2870
|
+
let validationFields = [];
|
|
2871
|
+
if (!flags.yes) {
|
|
2872
|
+
method = handleCancel(
|
|
2873
|
+
await p4__namespace.select({
|
|
2874
|
+
message: "HTTP Method:",
|
|
2875
|
+
options: [
|
|
2876
|
+
{ value: "get", label: "GET" },
|
|
2877
|
+
{ value: "post", label: "POST" },
|
|
2878
|
+
{ value: "put", label: "PUT" },
|
|
2879
|
+
{ value: "patch", label: "PATCH" },
|
|
2880
|
+
{ value: "delete", label: "DELETE" }
|
|
2881
|
+
],
|
|
2882
|
+
initialValue: method
|
|
2883
|
+
})
|
|
2884
|
+
);
|
|
2885
|
+
const defaultPath = method === "get" ? "/" : "/";
|
|
2886
|
+
subPath = handleCancel(
|
|
2887
|
+
await p4__namespace.text({
|
|
2888
|
+
message: "Route subpath (relative to feature prefix):",
|
|
2889
|
+
initialValue: defaultPath,
|
|
2890
|
+
defaultValue: defaultPath
|
|
2891
|
+
})
|
|
2892
|
+
).trim();
|
|
2893
|
+
const useCases = listUseCases(projectRoot, feature);
|
|
2894
|
+
if (useCases.length > 0) {
|
|
2895
|
+
const ucOptions = useCases.map((u) => ({ value: u.name, label: u.name }));
|
|
2896
|
+
ucOptions.push({ value: "__none__", label: "None / Standalone Handler" });
|
|
2897
|
+
const chosenUc = handleCancel(
|
|
2898
|
+
await p4__namespace.select({
|
|
2899
|
+
message: "Connect this handler to a domain Use Case?",
|
|
2900
|
+
options: ucOptions,
|
|
2901
|
+
initialValue: ucOptions[0]?.value
|
|
2902
|
+
})
|
|
2903
|
+
);
|
|
2904
|
+
if (chosenUc !== "__none__") {
|
|
2905
|
+
useCaseName = chosenUc;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
includeValidation = handleCancel(
|
|
2909
|
+
await p4__namespace.confirm({
|
|
2910
|
+
message: "Include Zod inbound request validation?",
|
|
2911
|
+
initialValue: method !== "delete"
|
|
2912
|
+
})
|
|
2913
|
+
);
|
|
2914
|
+
if (includeValidation) {
|
|
2915
|
+
const defaultSource = method === "get" ? "query" : "body";
|
|
2916
|
+
validationSource = handleCancel(
|
|
2917
|
+
await p4__namespace.select({
|
|
2918
|
+
message: "Validation data source:",
|
|
2919
|
+
options: [
|
|
2920
|
+
{ value: "body", label: "req.body (for POST / PUT / PATCH payloads)" },
|
|
2921
|
+
{ value: "query", label: "req.query (for GET search/filter queries)" },
|
|
2922
|
+
{ value: "params", label: "req.params (for route parameters e.g. :id)" }
|
|
2923
|
+
],
|
|
2924
|
+
initialValue: defaultSource
|
|
2925
|
+
})
|
|
2926
|
+
);
|
|
2927
|
+
validationFields = await promptProperties({
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
const s = p4__namespace.spinner();
|
|
2932
|
+
s.start(`Generating HTTP handler '${pc13__default.default.cyan(className)}'...`);
|
|
2933
|
+
const options = {
|
|
2934
|
+
name: className,
|
|
2935
|
+
feature,
|
|
2936
|
+
method,
|
|
2937
|
+
path: subPath,
|
|
2938
|
+
useCaseName,
|
|
2939
|
+
includeValidation,
|
|
2940
|
+
validationSource,
|
|
2941
|
+
validationFields,
|
|
2942
|
+
successMessage: flags.message,
|
|
2943
|
+
projectRoot,
|
|
2944
|
+
dryRun: Boolean(flags.dryRun),
|
|
2945
|
+
overwrite: Boolean(flags.overwrite)
|
|
2946
|
+
};
|
|
2947
|
+
const files = generateHttpHandler(options);
|
|
2948
|
+
s.stop("HTTP handler generated successfully!");
|
|
2949
|
+
printGeneratedFiles(files, options.dryRun);
|
|
2950
|
+
p4__namespace.outro(pc13__default.default.green(`HTTP handler '${className}' created in feature '${feature}'!`));
|
|
2951
|
+
}
|
|
2952
|
+
function generateInjectableCode(className, dependencies = [], methods = [], implementsInterface) {
|
|
2953
|
+
const formattedDeps = dependencies.map((dep) => ({
|
|
2954
|
+
key: toCamelCase(dep),
|
|
2955
|
+
name: dep
|
|
2956
|
+
}));
|
|
2957
|
+
const formattedMethods = methods.map((m) => {
|
|
2958
|
+
const isAsync = m.async !== false;
|
|
2959
|
+
const retType = m.returnType ? isAsync ? `Promise<${m.returnType}>` : m.returnType : isAsync ? "Promise<void>" : "void";
|
|
2960
|
+
const params = m.params && m.params.length > 0 ? m.params.map((p15) => `${p15.name}: ${p15.type}`).join(", ") : "";
|
|
2961
|
+
return {
|
|
2962
|
+
name: m.name,
|
|
2963
|
+
isAsync,
|
|
2964
|
+
params,
|
|
2965
|
+
returnType: retType
|
|
2966
|
+
};
|
|
2967
|
+
});
|
|
2968
|
+
return renderTemplate(injectableTemplate, {
|
|
2969
|
+
className,
|
|
2970
|
+
implementsInterface,
|
|
2971
|
+
dependencies: formattedDeps,
|
|
2972
|
+
hasDependencies: dependencies.length > 0,
|
|
2973
|
+
hasMethods: formattedMethods.length > 0,
|
|
2974
|
+
methods: formattedMethods
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
function generateInjectable(options) {
|
|
2978
|
+
const files = [];
|
|
2979
|
+
const projectRoot = options.projectRoot || findProjectRoot();
|
|
2980
|
+
const className = toPascalCase(options.name);
|
|
2981
|
+
let targetDir = "";
|
|
2982
|
+
if (options.customPath) {
|
|
2983
|
+
targetDir = path7__default.default.isAbsolute(options.customPath) ? options.customPath : path7__default.default.join(projectRoot, options.customPath);
|
|
2984
|
+
} else if (options.location === "feature-service" && options.feature) {
|
|
2985
|
+
targetDir = path7__default.default.join(resolveFeaturesDir(projectRoot), options.feature, "services");
|
|
2986
|
+
} else if (options.location === "feature-transformer" && options.feature) {
|
|
2987
|
+
targetDir = path7__default.default.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress", "transformers");
|
|
2988
|
+
} else if (options.location === "module") {
|
|
2989
|
+
targetDir = path7__default.default.join(projectRoot, "modules", className);
|
|
2990
|
+
} else {
|
|
2991
|
+
targetDir = options.feature ? path7__default.default.join(resolveFeaturesDir(projectRoot), options.feature, "services") : path7__default.default.join(projectRoot, "shared", className.toLowerCase());
|
|
2992
|
+
}
|
|
2993
|
+
const filePath = path7__default.default.join(targetDir, `${className}.ts`);
|
|
2994
|
+
let ifaceName;
|
|
2995
|
+
if (options.generateInterface) {
|
|
2996
|
+
ifaceName = `I${className}`;
|
|
2997
|
+
const ifacePath = path7__default.default.join(targetDir, `${ifaceName}.ts`);
|
|
2998
|
+
const ifaceContent = renderTemplate(injectableInterfaceTemplate, { ifaceName });
|
|
2999
|
+
files.push(
|
|
3000
|
+
writeFileSafely(ifacePath, ifaceContent, {
|
|
3001
|
+
projectRoot,
|
|
3002
|
+
dryRun: options.dryRun,
|
|
3003
|
+
overwrite: options.overwrite
|
|
3004
|
+
})
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
const code = generateInjectableCode(
|
|
3008
|
+
className,
|
|
3009
|
+
options.dependencies || [],
|
|
3010
|
+
options.methods || [],
|
|
3011
|
+
ifaceName
|
|
3012
|
+
);
|
|
3013
|
+
files.push(
|
|
3014
|
+
writeFileSafely(filePath, code, {
|
|
3015
|
+
projectRoot,
|
|
3016
|
+
dryRun: options.dryRun,
|
|
3017
|
+
overwrite: options.overwrite
|
|
3018
|
+
})
|
|
3019
|
+
);
|
|
3020
|
+
return files;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// src/commands/injectable.ts
|
|
3024
|
+
async function runInjectableCommand(classNameArg, flags = {}) {
|
|
3025
|
+
const projectRoot = findProjectRoot();
|
|
3026
|
+
const existingFeatures = listExistingFeatures(projectRoot);
|
|
3027
|
+
let name = classNameArg?.trim();
|
|
3028
|
+
if (!name) {
|
|
3029
|
+
name = handleCancel(
|
|
3030
|
+
await p4__namespace.text({
|
|
3031
|
+
message: "Injectable class name:",
|
|
3032
|
+
placeholder: "e.g. PasswordHasher, TokenService, UserTransformer",
|
|
3033
|
+
validate: (v) => v && v.trim() ? void 0 : "Class name cannot be empty"
|
|
3034
|
+
})
|
|
3035
|
+
).trim();
|
|
3036
|
+
}
|
|
3037
|
+
const className = toPascalCase(name);
|
|
3038
|
+
let location = "feature-service";
|
|
3039
|
+
let feature = flags.feature?.trim();
|
|
3040
|
+
let customPath;
|
|
3041
|
+
if (!flags.yes) {
|
|
3042
|
+
location = handleCancel(
|
|
3043
|
+
await p4__namespace.select({
|
|
3044
|
+
message: "Select target architectural location:",
|
|
3045
|
+
options: [
|
|
3046
|
+
{ value: "feature-service", label: "Feature Service (features/<feature>/services/)" },
|
|
3047
|
+
{ value: "feature-transformer", label: "Feature Transformer (features/<feature>/pxExpress/transformers/)" },
|
|
3048
|
+
{ value: "shared", label: "Shared Module (shared/<name>/)" },
|
|
3049
|
+
{ value: "module", label: "Core Module (modules/<Name>/)" },
|
|
3050
|
+
{ value: "custom", label: "Custom directory path..." }
|
|
3051
|
+
],
|
|
3052
|
+
initialValue: "feature-service"
|
|
3053
|
+
})
|
|
3054
|
+
);
|
|
3055
|
+
if (location === "feature-service" || location === "feature-transformer") {
|
|
3056
|
+
if (!feature) {
|
|
3057
|
+
if (existingFeatures.length > 0) {
|
|
3058
|
+
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
3059
|
+
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
3060
|
+
const selected = handleCancel(
|
|
3061
|
+
await p4__namespace.select({
|
|
3062
|
+
message: "Select target feature:",
|
|
3063
|
+
options: featureOptions
|
|
3064
|
+
})
|
|
3065
|
+
);
|
|
3066
|
+
if (selected === "__custom__") {
|
|
3067
|
+
feature = handleCancel(
|
|
3068
|
+
await p4__namespace.text({
|
|
3069
|
+
message: "Enter feature name:",
|
|
3070
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
3071
|
+
})
|
|
3072
|
+
).trim();
|
|
3073
|
+
} else {
|
|
3074
|
+
feature = selected;
|
|
3075
|
+
}
|
|
3076
|
+
} else {
|
|
3077
|
+
feature = handleCancel(
|
|
3078
|
+
await p4__namespace.text({
|
|
3079
|
+
message: "Enter target feature name:",
|
|
3080
|
+
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
3081
|
+
})
|
|
3082
|
+
).trim();
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
feature = toCamelCase(feature);
|
|
3086
|
+
} else if (location === "custom") {
|
|
3087
|
+
customPath = handleCancel(
|
|
3088
|
+
await p4__namespace.text({
|
|
3089
|
+
message: "Enter destination directory path:",
|
|
3090
|
+
placeholder: "e.g. src/services or lib/utils",
|
|
3091
|
+
validate: (v) => v && v.trim() ? void 0 : "Path cannot be empty"
|
|
3092
|
+
})
|
|
3093
|
+
).trim();
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
let generateInterface2 = false;
|
|
3097
|
+
if (!flags.yes) {
|
|
3098
|
+
generateInterface2 = handleCancel(
|
|
3099
|
+
await p4__namespace.confirm({
|
|
3100
|
+
message: `Generate abstract class interface (I${className}) as runtime DI token?`,
|
|
3101
|
+
initialValue: false
|
|
3102
|
+
})
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
const s = p4__namespace.spinner();
|
|
3106
|
+
s.start(`Generating injectable '${pc13__default.default.cyan(className)}'...`);
|
|
3107
|
+
const options = {
|
|
3108
|
+
name: className,
|
|
3109
|
+
feature,
|
|
3110
|
+
location,
|
|
3111
|
+
customPath,
|
|
3112
|
+
generateInterface: generateInterface2,
|
|
3113
|
+
projectRoot,
|
|
3114
|
+
dryRun: Boolean(flags.dryRun),
|
|
3115
|
+
overwrite: Boolean(flags.overwrite)
|
|
3116
|
+
};
|
|
3117
|
+
const files = generateInjectable(options);
|
|
3118
|
+
s.stop("Injectable class generated successfully!");
|
|
3119
|
+
printGeneratedFiles(files, options.dryRun);
|
|
3120
|
+
p4__namespace.outro(pc13__default.default.green(`Injectable '${className}' created successfully!`));
|
|
3121
|
+
}
|
|
3122
|
+
async function runInteractiveMenu(flags = {}) {
|
|
3123
|
+
printBanner();
|
|
3124
|
+
const choice = handleCancel(
|
|
3125
|
+
await p4__namespace.select({
|
|
3126
|
+
message: "What would you like to create in your architecture?",
|
|
3127
|
+
options: [
|
|
3128
|
+
{
|
|
3129
|
+
value: "feature",
|
|
3130
|
+
label: "Feature",
|
|
3131
|
+
hint: "Complete vertical slice (domain, infra, usecase, pxExpress, diProvider)"
|
|
3132
|
+
},
|
|
3133
|
+
{
|
|
3134
|
+
value: "usecase",
|
|
3135
|
+
label: "Use Case",
|
|
3136
|
+
hint: "Domain usecase with @MakeInjectable, DTOs & Vitest tests"
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
value: "domain",
|
|
3140
|
+
label: "Domain Entity / Model",
|
|
3141
|
+
hint: "Zero-dependency pure TypeScript interface, class, or value object"
|
|
3142
|
+
},
|
|
3143
|
+
{
|
|
3144
|
+
value: "interface",
|
|
3145
|
+
label: "Domain Interface",
|
|
3146
|
+
hint: "Abstract class repository/service interface serving as DI token"
|
|
3147
|
+
},
|
|
3148
|
+
{
|
|
3149
|
+
value: "infrastructure",
|
|
3150
|
+
label: "Infrastructure",
|
|
3151
|
+
hint: "In-memory stub or concrete repository/client adapter"
|
|
3152
|
+
},
|
|
3153
|
+
{
|
|
3154
|
+
value: "httpHandler",
|
|
3155
|
+
label: "HTTP Handler",
|
|
3156
|
+
hint: "ExpressRoute endpoint with @MakeInjectable & Zod validation"
|
|
3157
|
+
},
|
|
3158
|
+
{
|
|
3159
|
+
value: "injectable",
|
|
3160
|
+
label: "Injectable Service",
|
|
3161
|
+
hint: "Generic @MakeInjectable class for services, transformers, or modules"
|
|
3162
|
+
}
|
|
3163
|
+
]
|
|
3164
|
+
})
|
|
3165
|
+
);
|
|
3166
|
+
switch (choice) {
|
|
3167
|
+
case "feature":
|
|
3168
|
+
await runFeatureCommand(void 0, flags);
|
|
3169
|
+
break;
|
|
3170
|
+
case "usecase":
|
|
3171
|
+
await runUseCaseCommand(void 0, flags);
|
|
3172
|
+
break;
|
|
3173
|
+
case "domain":
|
|
3174
|
+
await runDomainCommand(void 0, flags);
|
|
3175
|
+
break;
|
|
3176
|
+
case "interface":
|
|
3177
|
+
await runInterfaceCommand(void 0, flags);
|
|
3178
|
+
break;
|
|
3179
|
+
case "infrastructure":
|
|
3180
|
+
await runInfrastructureCommand(void 0, flags);
|
|
3181
|
+
break;
|
|
3182
|
+
case "httpHandler":
|
|
3183
|
+
await runHttpHandlerCommand(void 0, flags);
|
|
3184
|
+
break;
|
|
3185
|
+
case "injectable":
|
|
3186
|
+
await runInjectableCommand(void 0, flags);
|
|
3187
|
+
break;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// src/commands/create.ts
|
|
3192
|
+
async function handleCreate(type, name, flags = {}) {
|
|
3193
|
+
if (!type) {
|
|
3194
|
+
await runInteractiveMenu(flags);
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
const normalizedType = type.trim();
|
|
3198
|
+
switch (normalizedType) {
|
|
3199
|
+
case "feature":
|
|
3200
|
+
await runFeatureCommand(name, flags);
|
|
3201
|
+
break;
|
|
3202
|
+
case "usecase":
|
|
3203
|
+
await runUseCaseCommand(name, flags);
|
|
3204
|
+
break;
|
|
3205
|
+
case "domain":
|
|
3206
|
+
await runDomainCommand(name, flags);
|
|
3207
|
+
break;
|
|
3208
|
+
case "infrastructure":
|
|
3209
|
+
await runInfrastructureCommand(name, flags);
|
|
3210
|
+
break;
|
|
3211
|
+
case "interface":
|
|
3212
|
+
await runInterfaceCommand(name, flags);
|
|
3213
|
+
break;
|
|
3214
|
+
case "httpHandler":
|
|
3215
|
+
case "httphandler":
|
|
3216
|
+
await runHttpHandlerCommand(name, flags);
|
|
3217
|
+
break;
|
|
3218
|
+
case "injectable":
|
|
3219
|
+
await runInjectableCommand(name, flags);
|
|
3220
|
+
break;
|
|
3221
|
+
default:
|
|
3222
|
+
p4__namespace.log.error(
|
|
3223
|
+
pc13__default.default.red(
|
|
3224
|
+
`Unknown component type: "${type}". Allowed types: feature, usecase, domain, infrastructure, interface, httpHandler, injectable.`
|
|
3225
|
+
)
|
|
3226
|
+
);
|
|
3227
|
+
process.exit(1);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
async function handleImport(typeArg, nameArg, flags = {}) {
|
|
3231
|
+
const projectRoot = findProjectRoot();
|
|
3232
|
+
let type = typeArg?.trim().toLowerCase();
|
|
3233
|
+
if (type === "features") type = "feature";
|
|
3234
|
+
if (!type) {
|
|
3235
|
+
type = handleCancel(
|
|
3236
|
+
await p4__namespace.select({
|
|
3237
|
+
message: "What kind of module would you like to import?",
|
|
3238
|
+
options: [
|
|
3239
|
+
{
|
|
3240
|
+
value: "shared",
|
|
3241
|
+
label: "Shared Module",
|
|
3242
|
+
hint: "Foundational utility or port placed in src/shared"
|
|
3243
|
+
},
|
|
3244
|
+
{
|
|
3245
|
+
value: "feature",
|
|
3246
|
+
label: "Feature",
|
|
3247
|
+
hint: "Complete Clean Architecture vertical slice placed in src/features"
|
|
3248
|
+
}
|
|
3249
|
+
]
|
|
3250
|
+
})
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
if (type !== "shared" && type !== "feature") {
|
|
3254
|
+
p4__namespace.log.error(pc13__default.default.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
|
|
3255
|
+
process.exit(1);
|
|
3256
|
+
}
|
|
3257
|
+
const registryBase = getRegistryBase(projectRoot, flags.registry);
|
|
3258
|
+
const s = p4__namespace.spinner();
|
|
3259
|
+
s.start(`Connecting to registry at ${pc13__default.default.dim(registryBase)}...`);
|
|
3260
|
+
let index;
|
|
3261
|
+
try {
|
|
3262
|
+
index = await fetchRegistryIndex(registryBase);
|
|
3263
|
+
s.stop("Registry index loaded.");
|
|
3264
|
+
} catch (err) {
|
|
3265
|
+
s.stop(pc13__default.default.red("Failed to load registry index."));
|
|
3266
|
+
p4__namespace.log.error(pc13__default.default.red(err.message || String(err)));
|
|
3267
|
+
process.exit(1);
|
|
3268
|
+
}
|
|
3269
|
+
const availableList = type === "shared" ? index.shared : index.features;
|
|
3270
|
+
const candidateNames = availableList.map((m) => m.name);
|
|
3271
|
+
let name = nameArg?.trim();
|
|
3272
|
+
if (!name) {
|
|
3273
|
+
if (availableList.length === 0) {
|
|
3274
|
+
p4__namespace.log.warn(`No ${type} modules available in the registry.`);
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
name = handleCancel(
|
|
3278
|
+
await p4__namespace.select({
|
|
3279
|
+
message: `Select a ${type} to import:`,
|
|
3280
|
+
options: availableList.map((item) => ({
|
|
3281
|
+
value: item.name,
|
|
3282
|
+
label: item.name,
|
|
3283
|
+
hint: item.description
|
|
3284
|
+
}))
|
|
3285
|
+
})
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3288
|
+
if (!candidateNames.includes(name)) {
|
|
3289
|
+
const closest = findClosestMatch(name, candidateNames);
|
|
3290
|
+
p4__namespace.log.error(pc13__default.default.red(`\u274C Module '${name}' not found in the ${type} registry.`));
|
|
3291
|
+
if (closest) {
|
|
3292
|
+
p4__namespace.log.info(pc13__default.default.yellow(`Did you mean '${pc13__default.default.cyan(closest)}'?`));
|
|
3293
|
+
}
|
|
3294
|
+
process.exit(1);
|
|
3295
|
+
}
|
|
3296
|
+
const importedModules = /* @__PURE__ */ new Set();
|
|
3297
|
+
const totalWrittenFiles = [];
|
|
3298
|
+
const requiredNpmPackages = /* @__PURE__ */ new Set();
|
|
3299
|
+
async function importModuleRecursive(modType, modName) {
|
|
3300
|
+
const modKey = `${modType}:${modName}`;
|
|
3301
|
+
if (importedModules.has(modKey)) return;
|
|
3302
|
+
p4__namespace.log.step(`Importing ${modType}: ${pc13__default.default.cyan(modName)}...`);
|
|
3303
|
+
let manifest;
|
|
3304
|
+
try {
|
|
3305
|
+
manifest = await fetchModuleManifest(registryBase, modType, modName);
|
|
3306
|
+
} catch (err) {
|
|
3307
|
+
p4__namespace.log.error(pc13__default.default.red(`Failed to fetch manifest for ${modType} '${modName}': ${err.message}`));
|
|
3308
|
+
process.exit(1);
|
|
3309
|
+
}
|
|
3310
|
+
const sharedDeps = manifest.dependencies?.shared || [];
|
|
3311
|
+
for (const dep of sharedDeps) {
|
|
3312
|
+
const depPath = path7__default.default.join(projectRoot, "src", "shared", dep);
|
|
3313
|
+
if (!fs3__default.default.existsSync(depPath) && !importedModules.has(`shared:${dep}`)) {
|
|
3314
|
+
p4__namespace.log.info(` \u21B3 Resolving required shared dependency: ${pc13__default.default.cyan(dep)}`);
|
|
3315
|
+
await importModuleRecursive("shared", dep);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
const featureDeps = manifest.dependencies?.features || [];
|
|
3319
|
+
for (const dep of featureDeps) {
|
|
3320
|
+
const depPath = path7__default.default.join(projectRoot, "src", "features", dep);
|
|
3321
|
+
if (!fs3__default.default.existsSync(depPath) && !importedModules.has(`feature:${dep}`)) {
|
|
3322
|
+
p4__namespace.log.info(` \u21B3 Resolving required feature dependency: ${pc13__default.default.cyan(dep)}`);
|
|
3323
|
+
await importModuleRecursive("feature", dep);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
for (const pkg of manifest.dependencies?.npm || []) {
|
|
3327
|
+
requiredNpmPackages.add(pkg);
|
|
3328
|
+
}
|
|
3329
|
+
const folder = modType === "shared" ? "shared" : "features";
|
|
3330
|
+
const targetDir = path7__default.default.join(projectRoot, "src", folder, modName);
|
|
3331
|
+
for (const file of manifest.files || []) {
|
|
3332
|
+
if (file === "registry.json") continue;
|
|
3333
|
+
const remoteFilePath = `${folder}/${modName}/${file}`;
|
|
3334
|
+
const localFilePath = path7__default.default.join(targetDir, file);
|
|
3335
|
+
if (fs3__default.default.existsSync(localFilePath) && !flags.overwrite) {
|
|
3336
|
+
p4__namespace.log.warn(` \u21B3 Skipping existing file: ${pc13__default.default.dim(path7__default.default.relative(projectRoot, localFilePath))} (use --overwrite to replace)`);
|
|
3337
|
+
continue;
|
|
3338
|
+
}
|
|
3339
|
+
const content = await fetchRegistryFile(registryBase, remoteFilePath);
|
|
3340
|
+
if (!flags.dryRun) {
|
|
3341
|
+
fs3__default.default.mkdirSync(path7__default.default.dirname(localFilePath), { recursive: true });
|
|
3342
|
+
fs3__default.default.writeFileSync(localFilePath, content, "utf-8");
|
|
3343
|
+
}
|
|
3344
|
+
const displayPath = path7__default.default.relative(projectRoot, localFilePath);
|
|
3345
|
+
totalWrittenFiles.push(displayPath);
|
|
3346
|
+
p4__namespace.log.message(` ${pc13__default.default.green("+")} ${displayPath}`);
|
|
3347
|
+
}
|
|
3348
|
+
importedModules.add(modKey);
|
|
3349
|
+
}
|
|
3350
|
+
await importModuleRecursive(type, name);
|
|
3351
|
+
const missingNpm = getMissingNpmPackages(projectRoot, Array.from(requiredNpmPackages));
|
|
3352
|
+
if (missingNpm.length > 0) {
|
|
3353
|
+
if (flags.skipInstall || flags.dryRun) {
|
|
3354
|
+
p4__namespace.log.info(`Required npm dependencies to install: ${pc13__default.default.cyan(missingNpm.join(", "))}`);
|
|
3355
|
+
} else {
|
|
3356
|
+
try {
|
|
3357
|
+
installNpmPackages(projectRoot, missingNpm);
|
|
3358
|
+
} catch (err) {
|
|
3359
|
+
p4__namespace.log.warn(`Could not automatically install packages (${err.message}). Please run: npm install ${missingNpm.join(" ")}`);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
if (flags.dryRun) {
|
|
3364
|
+
p4__namespace.log.info(pc13__default.default.yellow("Dry run complete: No files were written to disk."));
|
|
3365
|
+
} else {
|
|
3366
|
+
p4__namespace.log.success(
|
|
3367
|
+
pc13__default.default.green(
|
|
3368
|
+
`Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in src/${type === "shared" ? "shared" : "features"}/${name})`
|
|
3369
|
+
)
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// src/cli.ts
|
|
3375
|
+
function createCli() {
|
|
3376
|
+
const cli = cac.cac("mason");
|
|
3377
|
+
cli.command("[...args]", "Interactive scaffolding and import wizard").option("-d, --dry-run", "Preview changes without writing to disk").option("-o, --overwrite", "Overwrite existing files").option("-y, --yes", "Accept defaults and skip interactive prompts").action(async (args, flags) => {
|
|
3378
|
+
if (args && args.length > 0) {
|
|
3379
|
+
p4__namespace.log.error(
|
|
3380
|
+
pc13__default.default.red(
|
|
3381
|
+
`Unknown command: "${args.join(" ")}". Use --help to list available commands.`
|
|
3382
|
+
)
|
|
3383
|
+
);
|
|
3384
|
+
process.exit(1);
|
|
3385
|
+
}
|
|
3386
|
+
try {
|
|
3387
|
+
const choice = handleCancel(
|
|
3388
|
+
await p4__namespace.select({
|
|
3389
|
+
message: "\u{1F9F1} Mason: What would you like to do?",
|
|
3390
|
+
options: [
|
|
3391
|
+
{
|
|
3392
|
+
value: "create",
|
|
3393
|
+
label: "Create local component",
|
|
3394
|
+
hint: "Scaffold a new feature, use case, domain entity, interface, etc."
|
|
3395
|
+
},
|
|
3396
|
+
{
|
|
3397
|
+
value: "import",
|
|
3398
|
+
label: "Import from registry",
|
|
3399
|
+
hint: "Download and link pre-built shared modules or features from the registry"
|
|
3400
|
+
}
|
|
3401
|
+
]
|
|
3402
|
+
})
|
|
3403
|
+
);
|
|
3404
|
+
if (choice === "create") {
|
|
3405
|
+
await handleCreate(void 0, void 0, flags);
|
|
3406
|
+
} else {
|
|
3407
|
+
await handleImport(void 0, void 0, flags);
|
|
3408
|
+
}
|
|
3409
|
+
} catch (err) {
|
|
3410
|
+
handleCliError(err);
|
|
3411
|
+
}
|
|
3412
|
+
});
|
|
3413
|
+
cli.command(
|
|
3414
|
+
"create [type] [name]",
|
|
3415
|
+
"Scaffold local Clean Architecture components (feature, usecase, domain, etc.)"
|
|
3416
|
+
).option("-d, --dry-run", "Preview changes without writing to disk").option("-o, --overwrite", "Overwrite existing files").option("-y, --yes", "Accept defaults and skip interactive prompts").option("-f, --feature <feature>", "Target feature name").option("-t, --target <dir>", "Override target directory").option("--express", "Include Express presentation layer (pxExpress)").option("--no-express", "Skip Express presentation layer").option("-i, --interface <interface>", "Interface or abstract class to implement").option("-k, --kind <kind>", "Implementation kind ('stub' or 'concrete')").option("-g, --global", "Place in global infrastructure/ instead of feature").option("-m, --method <method>", "HTTP method (get, post, put, patch, delete)").option("-p, --path <path>", "Route subpath (e.g. /:id)").option("-u, --usecase <usecase>", "Connected use case name").option("--message <message>", "Custom success message").option("--domainNames <names>", "Comma-separated domain entity names").example("mason create").example("mason create feature users").example("mason create usecase PlaceOrder -f orders").example("mason create domain User -f users").example("mason create infrastructure PostgresUserRepository -f users -i IUserRepository").example("mason create interface IUserRepository -f users").example("mason create httpHandler GetUserHttp -f users -m get -p /:id").example("mason create injectable PasswordHasher").action(async (type, name, flags = {}) => {
|
|
3417
|
+
try {
|
|
3418
|
+
await handleCreate(type, name, flags);
|
|
3419
|
+
} catch (err) {
|
|
3420
|
+
handleCliError(err);
|
|
3421
|
+
}
|
|
3422
|
+
});
|
|
3423
|
+
cli.command(
|
|
3424
|
+
"import [type] [name]",
|
|
3425
|
+
"Import pre-built shared modules or features from the Solid Stack Registry"
|
|
3426
|
+
).option("-o, --overwrite", "Overwrite existing files").option("-d, --dry-run", "Preview changes without writing to disk").option("-y, --yes", "Accept defaults and skip interactive prompts").option("--registry <url>", "Registry base URL or local directory").option("--skip-install", "Skip automatic package manager dependency installation").example("mason import shared hasher").example("mason import feature authn").example("mason import shared").example("mason import feature").action(async (type, name, flags = {}) => {
|
|
3427
|
+
try {
|
|
3428
|
+
await handleImport(type, name, flags);
|
|
3429
|
+
} catch (err) {
|
|
3430
|
+
handleCliError(err);
|
|
3431
|
+
}
|
|
3432
|
+
});
|
|
3433
|
+
const createSubtypes = [
|
|
3434
|
+
"feature",
|
|
3435
|
+
"usecase",
|
|
3436
|
+
"domain",
|
|
3437
|
+
"infrastructure",
|
|
3438
|
+
"interface",
|
|
3439
|
+
"httpHandler",
|
|
3440
|
+
"injectable"
|
|
3441
|
+
];
|
|
3442
|
+
for (const subtype of createSubtypes) {
|
|
3443
|
+
cli.command(
|
|
3444
|
+
`${subtype} [name]`,
|
|
3445
|
+
`Shorthand for 'mason create ${subtype} [name]'`
|
|
3446
|
+
).allowUnknownOptions().action(async (name, flags = {}) => {
|
|
3447
|
+
try {
|
|
3448
|
+
await handleCreate(subtype, name, flags);
|
|
3449
|
+
} catch (err) {
|
|
3450
|
+
handleCliError(err);
|
|
3451
|
+
}
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
cli.help();
|
|
3455
|
+
cli.version("1.0.10");
|
|
3456
|
+
return cli;
|
|
3457
|
+
}
|
|
3458
|
+
function handleCliError(err) {
|
|
3459
|
+
if (err instanceof PromptCancelledError) {
|
|
3460
|
+
process.exit(0);
|
|
3461
|
+
}
|
|
3462
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3463
|
+
p4__namespace.log.error(pc13__default.default.red(`Error: ${message}`));
|
|
3464
|
+
process.exit(1);
|
|
3465
|
+
}
|
|
3466
|
+
async function runCli(argv = process.argv) {
|
|
3467
|
+
const cli = createCli();
|
|
3468
|
+
cli.parse(argv, { run: true });
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
exports.DEFAULT_REGISTRY_BASE = DEFAULT_REGISTRY_BASE;
|
|
3472
|
+
exports.PromptCancelledError = PromptCancelledError;
|
|
3473
|
+
exports.concreteInfraTemplate = concreteInfraTemplate;
|
|
3474
|
+
exports.createCli = createCli;
|
|
3475
|
+
exports.detectPackageManager = detectPackageManager;
|
|
3476
|
+
exports.diProviderTemplate = diProviderTemplate;
|
|
3477
|
+
exports.domainClassTemplate = domainClassTemplate;
|
|
3478
|
+
exports.domainInterfaceTemplate = domainInterfaceTemplate;
|
|
3479
|
+
exports.domainValueObjectTemplate = domainValueObjectTemplate;
|
|
3480
|
+
exports.ensureInterfacePrefix = ensureInterfacePrefix;
|
|
3481
|
+
exports.expressIndexTemplate = expressIndexTemplate;
|
|
3482
|
+
exports.fetchModuleManifest = fetchModuleManifest;
|
|
3483
|
+
exports.fetchRegistryFile = fetchRegistryFile;
|
|
3484
|
+
exports.fetchRegistryIndex = fetchRegistryIndex;
|
|
3485
|
+
exports.findClosestMatch = findClosestMatch;
|
|
3486
|
+
exports.findInterfaceFile = findInterfaceFile;
|
|
3487
|
+
exports.findProjectRoot = findProjectRoot;
|
|
3488
|
+
exports.generateConcreteInfraCode = generateConcreteInfraCode;
|
|
3489
|
+
exports.generateDomain = generateDomain;
|
|
3490
|
+
exports.generateDomainEntityCode = generateDomainEntityCode;
|
|
3491
|
+
exports.generateFeature = generateFeature;
|
|
3492
|
+
exports.generateHttpHandler = generateHttpHandler;
|
|
3493
|
+
exports.generateHttpHandlerCode = generateHttpHandlerCode;
|
|
3494
|
+
exports.generateInfrastructure = generateInfrastructure;
|
|
3495
|
+
exports.generateInjectable = generateInjectable;
|
|
3496
|
+
exports.generateInjectableCode = generateInjectableCode;
|
|
3497
|
+
exports.generateInterface = generateInterface;
|
|
3498
|
+
exports.generateInterfaceCode = generateInterfaceCode;
|
|
3499
|
+
exports.generateStubCode = generateStubCode;
|
|
3500
|
+
exports.generateUseCase = generateUseCase;
|
|
3501
|
+
exports.generateUseCaseCode = generateUseCaseCode;
|
|
3502
|
+
exports.generateUseCaseTestCode = generateUseCaseTestCode;
|
|
3503
|
+
exports.getMissingNpmPackages = getMissingNpmPackages;
|
|
3504
|
+
exports.getRegistryBase = getRegistryBase;
|
|
3505
|
+
exports.getTemplatesDir = getTemplatesDir;
|
|
3506
|
+
exports.handleCancel = handleCancel;
|
|
3507
|
+
exports.handleCreate = handleCreate;
|
|
3508
|
+
exports.handleImport = handleImport;
|
|
3509
|
+
exports.hbs = hbs;
|
|
3510
|
+
exports.httpHandlerTemplate = httpHandlerTemplate;
|
|
3511
|
+
exports.injectableInterfaceTemplate = injectableInterfaceTemplate;
|
|
3512
|
+
exports.injectableTemplate = injectableTemplate;
|
|
3513
|
+
exports.installNpmPackages = installNpmPackages;
|
|
3514
|
+
exports.interfaceTemplate = interfaceTemplate;
|
|
3515
|
+
exports.isValidIdentifier = isValidIdentifier;
|
|
3516
|
+
exports.levenshteinDistance = levenshteinDistance;
|
|
3517
|
+
exports.listDomainEntities = listDomainEntities;
|
|
3518
|
+
exports.listDomainInterfaces = listDomainInterfaces;
|
|
3519
|
+
exports.listExistingFeatures = listExistingFeatures;
|
|
3520
|
+
exports.listUseCases = listUseCases;
|
|
3521
|
+
exports.loadInterfaceHierarchy = loadInterfaceHierarchy;
|
|
3522
|
+
exports.loadTemplate = loadTemplate;
|
|
3523
|
+
exports.parseInterfaceMethodsAndImports = parseInterfaceMethodsAndImports;
|
|
3524
|
+
exports.printBanner = printBanner;
|
|
3525
|
+
exports.printGeneratedFiles = printGeneratedFiles;
|
|
3526
|
+
exports.promptProperties = promptProperties;
|
|
3527
|
+
exports.removeInterfacePrefix = removeInterfacePrefix;
|
|
3528
|
+
exports.renderTemplate = renderTemplate;
|
|
3529
|
+
exports.renderTemplateFile = renderTemplateFile;
|
|
3530
|
+
exports.resolveFeaturesDir = resolveFeaturesDir;
|
|
3531
|
+
exports.runCli = runCli;
|
|
3532
|
+
exports.stubInfraTemplate = stubInfraTemplate;
|
|
3533
|
+
exports.synthesizeCrudMethods = synthesizeCrudMethods;
|
|
3534
|
+
exports.toCamelCase = toCamelCase;
|
|
3535
|
+
exports.toKebabCase = toKebabCase;
|
|
3536
|
+
exports.toPascalCase = toPascalCase;
|
|
3537
|
+
exports.toPlural = toPlural;
|
|
3538
|
+
exports.toSingular = toSingular;
|
|
3539
|
+
exports.toStubName = toStubName;
|
|
3540
|
+
exports.updateOrGenerateDiProvider = updateOrGenerateDiProvider;
|
|
3541
|
+
exports.useCaseTemplate = useCaseTemplate;
|
|
3542
|
+
exports.useCaseTestTemplate = useCaseTestTemplate;
|
|
3543
|
+
exports.writeFileSafely = writeFileSafely;
|
|
3544
|
+
//# sourceMappingURL=index.cjs.map
|
|
3545
|
+
//# sourceMappingURL=index.cjs.map
|