@0kmpo/openapi-clean-arch-generator 1.3.14 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -63
- package/dist/main.js +31 -3
- package/dist/package.json +1 -1
- package/dist/src/generators/clean-arch.generator.js +221 -145
- package/dist/src/generators/dto.generator.js +48 -24
- package/dist/src/generators/report.generator.js +6 -0
- package/dist/src/utils/config.js +3 -0
- package/dist/src/utils/example-validator.js +86 -0
- package/dist/src/utils/mock-value-resolver.js +19 -7
- package/dist/src/utils/name-formatter.js +133 -2
- package/dist/templates/api.repository.contract.mustache +1 -1
- package/dist/templates/api.repository.impl.mock.mustache +2 -2
- package/dist/templates/api.repository.impl.mustache +5 -5
- package/dist/templates/api.repository.impl.spec.mustache +2 -2
- package/dist/templates/api.use-cases.contract.mustache +1 -1
- package/dist/templates/api.use-cases.impl.mustache +2 -2
- package/dist/templates/api.use-cases.impl.spec.mustache +2 -2
- package/dist/templates/api.use-cases.mock.mustache +2 -2
- package/dist/templates/dto.mock.mustache +1 -1
- package/dist/templates/mapper.mustache +2 -2
- package/dist/templates/mapper.spec.mustache +2 -2
- package/dist/templates/model-entity.mustache +1 -1
- package/dist/templates/model-entity.spec.mustache +4 -0
- package/dist/templates/model.mock.mustache +2 -2
- package/dist/templates/repository.provider.mock.mustache +2 -2
- package/dist/templates/repository.provider.mustache +2 -2
- package/dist/templates/use-cases.provider.mock.mustache +2 -2
- package/dist/templates/use-cases.provider.mustache +2 -2
- package/package.json +1 -1
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.extractTagsFromAnalysis = extractTagsFromAnalysis;
|
|
7
7
|
exports.extractTagsWithOperations = extractTagsWithOperations;
|
|
8
|
+
exports.buildTagsMapFromAnalysis = buildTagsMapFromAnalysis;
|
|
9
|
+
exports.buildSchemaTagMap = buildSchemaTagMap;
|
|
8
10
|
exports.generateCleanArchitecture = generateCleanArchitecture;
|
|
9
11
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
12
|
const path_1 = __importDefault(require("path"));
|
|
@@ -57,8 +59,139 @@ function extractTagsWithOperations(analysis) {
|
|
|
57
59
|
});
|
|
58
60
|
return [...map.values()];
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Builds and returns the tagsMap from the swagger analysis, applying the optional selection filter.
|
|
64
|
+
* Exported so callers (e.g. main.ts) can compute it before organizeFiles runs.
|
|
65
|
+
*/
|
|
66
|
+
function buildTagsMapFromAnalysis(analysis, selectionFilter = {}) {
|
|
67
|
+
const tagsMap = {};
|
|
68
|
+
Object.keys(analysis.paths).forEach((pathKey) => {
|
|
69
|
+
const pathObj = analysis.paths[pathKey];
|
|
70
|
+
Object.keys(pathObj).forEach((method) => {
|
|
71
|
+
const op = pathObj[method];
|
|
72
|
+
if (op.tags && op.tags.length > 0) {
|
|
73
|
+
const tag = op.tags[0];
|
|
74
|
+
if (!tagsMap[tag])
|
|
75
|
+
tagsMap[tag] = [];
|
|
76
|
+
const allParams = (op.parameters || []).map((p) => ({
|
|
77
|
+
paramName: p.name,
|
|
78
|
+
dataType: (0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''),
|
|
79
|
+
description: p.description || '',
|
|
80
|
+
required: p.required,
|
|
81
|
+
testValue: resolveTestParamValue((0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''))
|
|
82
|
+
}));
|
|
83
|
+
if (op.requestBody) {
|
|
84
|
+
let bodyType = 'unknown';
|
|
85
|
+
const content = op.requestBody.content?.['application/json']?.schema;
|
|
86
|
+
if (content) {
|
|
87
|
+
if (content.$ref)
|
|
88
|
+
bodyType = content.$ref.split('/').pop() || 'unknown';
|
|
89
|
+
else if (content.type)
|
|
90
|
+
bodyType = (0, type_mapper_1.mapSwaggerTypeToTs)(content.type);
|
|
91
|
+
}
|
|
92
|
+
allParams.push({
|
|
93
|
+
paramName: 'body',
|
|
94
|
+
dataType: bodyType,
|
|
95
|
+
description: op.requestBody.description || '',
|
|
96
|
+
required: true,
|
|
97
|
+
testValue: resolveTestParamValue(bodyType)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
let returnType = 'void';
|
|
101
|
+
let returnBaseType = 'void';
|
|
102
|
+
let isListContainer = false;
|
|
103
|
+
const successCode = ['200', '201', '202', '203'].find((code) => op.responses?.[code]);
|
|
104
|
+
const responseSchema = successCode !== undefined
|
|
105
|
+
? op.responses?.[successCode]?.content?.['application/json']?.schema
|
|
106
|
+
: undefined;
|
|
107
|
+
if (responseSchema) {
|
|
108
|
+
if (responseSchema.$ref) {
|
|
109
|
+
returnType = responseSchema.$ref.split('/').pop() || 'unknown';
|
|
110
|
+
returnBaseType = returnType;
|
|
111
|
+
}
|
|
112
|
+
else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
|
|
113
|
+
returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
|
|
114
|
+
returnType = `${returnBaseType}[]`;
|
|
115
|
+
isListContainer = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
|
|
119
|
+
const hasBodyParam = !!op.requestBody;
|
|
120
|
+
// Sort: required params first, optional params last (TypeScript requirement)
|
|
121
|
+
allParams.sort((a, b) => {
|
|
122
|
+
if (a.required === b.required)
|
|
123
|
+
return 0;
|
|
124
|
+
return a.required ? -1 : 1;
|
|
125
|
+
});
|
|
126
|
+
tagsMap[tag].push({
|
|
127
|
+
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
|
|
128
|
+
summary: op.summary || '',
|
|
129
|
+
notes: op.description || '',
|
|
130
|
+
httpMethod: method.toLowerCase(),
|
|
131
|
+
uppercaseHttpMethod: method.toUpperCase(),
|
|
132
|
+
path: pathKey,
|
|
133
|
+
allParams: allParams.map((p, i) => ({
|
|
134
|
+
...p,
|
|
135
|
+
'-last': i === allParams.length - 1
|
|
136
|
+
})),
|
|
137
|
+
hasQueryParams,
|
|
138
|
+
queryParams: (op.parameters || [])
|
|
139
|
+
.filter((p) => p.in === 'query')
|
|
140
|
+
.map((p, i, arr) => ({
|
|
141
|
+
paramName: p.name,
|
|
142
|
+
'-last': i === arr.length - 1
|
|
143
|
+
})),
|
|
144
|
+
hasBodyParam,
|
|
145
|
+
bodyParam: 'body',
|
|
146
|
+
hasOptions: hasQueryParams || hasBodyParam,
|
|
147
|
+
hasBothParamsAndBody: hasQueryParams && hasBodyParam,
|
|
148
|
+
returnType: returnType !== 'void' ? returnType : false,
|
|
149
|
+
returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
|
|
150
|
+
returnTypeVarName: returnType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnType) : false,
|
|
151
|
+
returnBaseTypeVarName: returnBaseType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnBaseType) : false,
|
|
152
|
+
isListContainer: isListContainer,
|
|
153
|
+
vendorExtensions: {}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
if (Object.keys(selectionFilter).length > 0) {
|
|
159
|
+
Object.keys(tagsMap).forEach((tag) => {
|
|
160
|
+
if (!selectionFilter[tag]) {
|
|
161
|
+
delete tagsMap[tag];
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname));
|
|
165
|
+
if (tagsMap[tag].length === 0)
|
|
166
|
+
delete tagsMap[tag];
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return tagsMap;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Maps each schema basename to the tag subfolder it belongs to.
|
|
174
|
+
* Schemas used by exactly one tag → that tag's camelCase name.
|
|
175
|
+
* Schemas used by 0 or multiple tags → 'shared'.
|
|
176
|
+
*/
|
|
177
|
+
function buildSchemaTagMap(schemas, tagsMap) {
|
|
178
|
+
const result = {};
|
|
179
|
+
Object.keys(schemas).forEach((schemaName) => {
|
|
180
|
+
const baseName = schemaName.replace(/Dto$/, '');
|
|
181
|
+
const tagsUsing = [];
|
|
182
|
+
Object.keys(tagsMap).forEach((tag) => {
|
|
183
|
+
const used = tagsMap[tag].some((op) => op.returnType === baseName ||
|
|
184
|
+
op.returnType === `${baseName}[]` ||
|
|
185
|
+
op.allParams.some((p) => p.dataType === baseName || p.dataType === `${baseName}[]`));
|
|
186
|
+
if (used)
|
|
187
|
+
tagsUsing.push(tag);
|
|
188
|
+
});
|
|
189
|
+
result[baseName] = tagsUsing.length === 1 ? (0, name_formatter_1.toCamelCase)(tagsUsing[0]) : 'shared';
|
|
190
|
+
});
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
60
193
|
/** Generates all Clean Architecture artefacts (models, mappers, repos, use cases, providers) using Mustache. */
|
|
61
|
-
function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyMap = {}, selectionFilter = {}) {
|
|
194
|
+
function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyMap = {}, selectionFilter = {}, precomputedSchemaTagMap = {}, baseName = '') {
|
|
62
195
|
(0, logger_1.logStep)('Generating Clean Architecture artefacts using Mustache...');
|
|
63
196
|
const generatedCount = {
|
|
64
197
|
models: 0,
|
|
@@ -71,14 +204,23 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
71
204
|
};
|
|
72
205
|
const schemas = analysis.swagger.components
|
|
73
206
|
?.schemas || {};
|
|
207
|
+
// Build tagsMap first — needed to compute schemaTagMap before the schema loop
|
|
208
|
+
const tagsMap = buildTagsMapFromAnalysis(analysis, selectionFilter);
|
|
209
|
+
// Map each schema basename → tag subfolder ('shared' if used by 0 or >1 tags)
|
|
210
|
+
const schemaTagMap = Object.keys(precomputedSchemaTagMap).length > 0
|
|
211
|
+
? precomputedSchemaTagMap
|
|
212
|
+
: buildSchemaTagMap(schemas, tagsMap);
|
|
74
213
|
// 1. Generate Models, Entities and Mappers from Schemas
|
|
75
214
|
Object.keys(schemas).forEach((schemaName) => {
|
|
76
|
-
const
|
|
215
|
+
const schemaBaseName = schemaName.replace(/Dto$/, '');
|
|
216
|
+
const tagFilename = schemaTagMap[schemaBaseName] || 'shared';
|
|
217
|
+
const tagFolderPath = baseName ? `${baseName}/${tagFilename}` : tagFilename;
|
|
77
218
|
const schemaObj = schemas[schemaName];
|
|
78
219
|
const rawProperties = schemaObj.properties || {};
|
|
79
220
|
const requiredProps = schemaObj.required || [];
|
|
80
221
|
const varsMap = Object.keys(rawProperties).map((k) => {
|
|
81
222
|
let tsType = (0, type_mapper_1.mapSwaggerTypeToTs)(rawProperties[k].type);
|
|
223
|
+
const isInlineObject = rawProperties[k].type === 'object' && !rawProperties[k].$ref;
|
|
82
224
|
if (rawProperties[k].$ref) {
|
|
83
225
|
tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
|
|
84
226
|
}
|
|
@@ -86,10 +228,12 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
86
228
|
tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
|
|
87
229
|
}
|
|
88
230
|
return {
|
|
89
|
-
name: k,
|
|
231
|
+
name: (0, name_formatter_1.safePropertyName)(k),
|
|
232
|
+
originalName: k,
|
|
90
233
|
dataType: tsType,
|
|
91
234
|
description: rawProperties[k].description || '',
|
|
92
|
-
required: requiredProps.includes(k)
|
|
235
|
+
required: requiredProps.includes(k),
|
|
236
|
+
hasMockValue: !isInlineObject
|
|
93
237
|
};
|
|
94
238
|
});
|
|
95
239
|
// Collect imports for types referenced via $ref in properties
|
|
@@ -102,16 +246,23 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
102
246
|
referencedTypes.add(prop.items.$ref.split('/').pop() || '');
|
|
103
247
|
}
|
|
104
248
|
});
|
|
105
|
-
const modelImports = [...referencedTypes]
|
|
106
|
-
|
|
107
|
-
|
|
249
|
+
const modelImports = [...referencedTypes].filter(Boolean).map((name) => ({
|
|
250
|
+
classname: name,
|
|
251
|
+
classFilename: (0, name_formatter_1.toCamelCase)(name),
|
|
252
|
+
tagFilename: schemaTagMap[name] || 'shared',
|
|
253
|
+
tagFolderPath: baseName
|
|
254
|
+
? `${baseName}/${schemaTagMap[name] || 'shared'}`
|
|
255
|
+
: schemaTagMap[name] || 'shared'
|
|
256
|
+
}));
|
|
108
257
|
const modelViewData = {
|
|
258
|
+
tagFilename,
|
|
259
|
+
tagFolderPath,
|
|
109
260
|
models: [
|
|
110
261
|
{
|
|
111
262
|
model: {
|
|
112
|
-
classname:
|
|
113
|
-
classFilename: (0, name_formatter_1.toCamelCase)(
|
|
114
|
-
classVarName: (0, name_formatter_1.toCamelCase)(
|
|
263
|
+
classname: schemaBaseName,
|
|
264
|
+
classFilename: (0, name_formatter_1.toCamelCase)(schemaBaseName),
|
|
265
|
+
classVarName: (0, name_formatter_1.toCamelCase)(schemaBaseName),
|
|
115
266
|
description: schemaObj.description || '',
|
|
116
267
|
imports: modelImports,
|
|
117
268
|
vars: varsMap
|
|
@@ -126,9 +277,11 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
126
277
|
apis: [
|
|
127
278
|
{
|
|
128
279
|
operations: {
|
|
129
|
-
classname:
|
|
130
|
-
classFilename: (0, name_formatter_1.toCamelCase)(
|
|
131
|
-
classVarName: (0, name_formatter_1.toCamelCase)(
|
|
280
|
+
classname: schemaBaseName,
|
|
281
|
+
classFilename: (0, name_formatter_1.toCamelCase)(schemaBaseName),
|
|
282
|
+
classVarName: (0, name_formatter_1.toCamelCase)(schemaBaseName),
|
|
283
|
+
tagFilename,
|
|
284
|
+
tagFolderPath
|
|
132
285
|
}
|
|
133
286
|
}
|
|
134
287
|
]
|
|
@@ -139,7 +292,8 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
139
292
|
if (fs_extra_1.default.existsSync(modelTemplatePath)) {
|
|
140
293
|
const template = fs_extra_1.default.readFileSync(modelTemplatePath, 'utf8');
|
|
141
294
|
const output = mustache_1.default.render(template, modelViewData);
|
|
142
|
-
const destPath = path_1.default.join(outputDir, 'entities/models', `${(0, name_formatter_1.toCamelCase)(
|
|
295
|
+
const destPath = path_1.default.join(outputDir, 'entities/models', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.model.ts`);
|
|
296
|
+
fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
|
|
143
297
|
fs_extra_1.default.writeFileSync(destPath, output);
|
|
144
298
|
generatedCount.models++;
|
|
145
299
|
(0, logger_1.logDetail)('generate', `model-entity → ${path_1.default.relative(process.cwd(), destPath)}`);
|
|
@@ -149,145 +303,69 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
149
303
|
if (fs_extra_1.default.existsSync(mapperTemplatePath)) {
|
|
150
304
|
const template = fs_extra_1.default.readFileSync(mapperTemplatePath, 'utf8');
|
|
151
305
|
const output = mustache_1.default.render(template, mapperViewData);
|
|
152
|
-
const destPath = path_1.default.join(outputDir, 'data/mappers', `${(0, name_formatter_1.toCamelCase)(
|
|
306
|
+
const destPath = path_1.default.join(outputDir, 'data/mappers', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.mapper.ts`);
|
|
307
|
+
fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
|
|
153
308
|
fs_extra_1.default.writeFileSync(destPath, output);
|
|
154
309
|
generatedCount.mappers++;
|
|
155
310
|
}
|
|
156
311
|
// DTO mock — values resolved from raw schema (example, format, type)
|
|
157
312
|
const dtoMockVarsMap = Object.keys(rawProperties).map((k) => ({
|
|
158
|
-
name: k,
|
|
159
|
-
mockValue: (0, mock_value_resolver_1.resolveMockValue)(k, rawProperties[k], 'dto')
|
|
313
|
+
name: (0, name_formatter_1.safePropertyName)(k),
|
|
314
|
+
mockValue: (0, mock_value_resolver_1.resolveMockValue)(k, rawProperties[k], 'dto', schemaName)
|
|
160
315
|
}));
|
|
161
|
-
const dtoMockImports = [...referencedTypes]
|
|
162
|
-
|
|
163
|
-
|
|
316
|
+
const dtoMockImports = [...referencedTypes].filter(Boolean).map((name) => {
|
|
317
|
+
const targetTag = schemaTagMap[name] || 'shared';
|
|
318
|
+
const targetFile = `${(0, name_formatter_1.toCamelCase)(name)}.dto.mock`;
|
|
319
|
+
const importPath = targetTag === tagFilename ? `./${targetFile}` : `../${targetTag}/${targetFile}`;
|
|
320
|
+
return {
|
|
321
|
+
classname: name,
|
|
322
|
+
classFilename: (0, name_formatter_1.toCamelCase)(name),
|
|
323
|
+
tagFilename: targetTag,
|
|
324
|
+
tagFolderPath: baseName ? `${baseName}/${targetTag}` : targetTag,
|
|
325
|
+
importPath
|
|
326
|
+
};
|
|
327
|
+
});
|
|
164
328
|
const dtoMockViewData = {
|
|
329
|
+
tagFilename,
|
|
330
|
+
tagFolderPath,
|
|
165
331
|
models: [
|
|
166
332
|
{
|
|
167
333
|
model: {
|
|
168
|
-
classname:
|
|
169
|
-
classFilename: (0, name_formatter_1.toCamelCase)(
|
|
170
|
-
classVarName: (0, name_formatter_1.toCamelCase)(
|
|
334
|
+
classname: schemaBaseName,
|
|
335
|
+
classFilename: (0, name_formatter_1.toCamelCase)(schemaBaseName),
|
|
336
|
+
classVarName: (0, name_formatter_1.toCamelCase)(schemaBaseName),
|
|
171
337
|
mockImports: dtoMockImports,
|
|
172
338
|
vars: dtoMockVarsMap
|
|
173
339
|
}
|
|
174
340
|
}
|
|
175
341
|
]
|
|
176
342
|
};
|
|
177
|
-
renderTemplate(templatesDir, 'dto.mock.mustache', dtoMockViewData, path_1.default.join(outputDir, 'data/dtos', `${(0, name_formatter_1.toCamelCase)(
|
|
343
|
+
renderTemplate(templatesDir, 'dto.mock.mustache', dtoMockViewData, path_1.default.join(outputDir, 'data/dtos', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.dto.mock.ts`), generatedCount, 'mocks');
|
|
178
344
|
// Model mock — delegates to mapper + DTO mock (no property values needed)
|
|
179
|
-
renderTemplate(templatesDir, 'model.mock.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', `${(0, name_formatter_1.toCamelCase)(
|
|
345
|
+
renderTemplate(templatesDir, 'model.mock.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.model.mock.ts`), generatedCount, 'mocks');
|
|
180
346
|
// Model spec
|
|
181
|
-
renderTemplate(templatesDir, 'model-entity.spec.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', `${(0, name_formatter_1.toCamelCase)(
|
|
347
|
+
renderTemplate(templatesDir, 'model-entity.spec.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.model.spec.ts`), generatedCount, 'specs');
|
|
182
348
|
// Mapper spec
|
|
183
|
-
renderTemplate(templatesDir, 'mapper.spec.mustache', mapperViewData, path_1.default.join(outputDir, 'data/mappers', `${(0, name_formatter_1.toCamelCase)(
|
|
349
|
+
renderTemplate(templatesDir, 'mapper.spec.mustache', mapperViewData, path_1.default.join(outputDir, 'data/mappers', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.mapper.spec.ts`), generatedCount, 'specs');
|
|
184
350
|
});
|
|
185
351
|
// 2. Generate Use Cases and Repositories from Paths/Tags
|
|
186
|
-
const tagsMap = {};
|
|
187
|
-
Object.keys(analysis.paths).forEach((pathKey) => {
|
|
188
|
-
const pathObj = analysis.paths[pathKey];
|
|
189
|
-
Object.keys(pathObj).forEach((method) => {
|
|
190
|
-
const op = pathObj[method];
|
|
191
|
-
if (op.tags && op.tags.length > 0) {
|
|
192
|
-
const tag = op.tags[0];
|
|
193
|
-
if (!tagsMap[tag])
|
|
194
|
-
tagsMap[tag] = [];
|
|
195
|
-
const allParams = (op.parameters || []).map((p) => ({
|
|
196
|
-
paramName: p.name,
|
|
197
|
-
dataType: (0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''),
|
|
198
|
-
description: p.description || '',
|
|
199
|
-
required: p.required,
|
|
200
|
-
testValue: resolveTestParamValue((0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''))
|
|
201
|
-
}));
|
|
202
|
-
if (op.requestBody) {
|
|
203
|
-
let bodyType = 'unknown';
|
|
204
|
-
const content = op.requestBody.content?.['application/json']?.schema;
|
|
205
|
-
if (content) {
|
|
206
|
-
if (content.$ref)
|
|
207
|
-
bodyType = content.$ref.split('/').pop() || 'unknown';
|
|
208
|
-
else if (content.type)
|
|
209
|
-
bodyType = (0, type_mapper_1.mapSwaggerTypeToTs)(content.type);
|
|
210
|
-
}
|
|
211
|
-
allParams.push({
|
|
212
|
-
paramName: 'body',
|
|
213
|
-
dataType: bodyType,
|
|
214
|
-
description: op.requestBody.description || '',
|
|
215
|
-
required: true,
|
|
216
|
-
testValue: resolveTestParamValue(bodyType)
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
let returnType = 'void';
|
|
220
|
-
let returnBaseType = 'void';
|
|
221
|
-
let isListContainer = false;
|
|
222
|
-
const successCode = ['200', '201', '202', '203'].find((code) => op.responses?.[code]);
|
|
223
|
-
const responseSchema = successCode !== undefined
|
|
224
|
-
? op.responses?.[successCode]?.content?.['application/json']?.schema
|
|
225
|
-
: undefined;
|
|
226
|
-
if (responseSchema) {
|
|
227
|
-
if (responseSchema.$ref) {
|
|
228
|
-
returnType = responseSchema.$ref.split('/').pop() || 'unknown';
|
|
229
|
-
returnBaseType = returnType;
|
|
230
|
-
}
|
|
231
|
-
else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
|
|
232
|
-
returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
|
|
233
|
-
returnType = `${returnBaseType}[]`;
|
|
234
|
-
isListContainer = true;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
|
|
238
|
-
const hasBodyParam = !!op.requestBody;
|
|
239
|
-
tagsMap[tag].push({
|
|
240
|
-
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
|
|
241
|
-
summary: op.summary || '',
|
|
242
|
-
notes: op.description || '',
|
|
243
|
-
httpMethod: method.toLowerCase(),
|
|
244
|
-
uppercaseHttpMethod: method.toUpperCase(),
|
|
245
|
-
path: pathKey,
|
|
246
|
-
allParams: allParams.map((p, i) => ({
|
|
247
|
-
...p,
|
|
248
|
-
'-last': i === allParams.length - 1
|
|
249
|
-
})),
|
|
250
|
-
hasQueryParams,
|
|
251
|
-
queryParams: (op.parameters || [])
|
|
252
|
-
.filter((p) => p.in === 'query')
|
|
253
|
-
.map((p, i, arr) => ({
|
|
254
|
-
paramName: p.name,
|
|
255
|
-
'-last': i === arr.length - 1
|
|
256
|
-
})),
|
|
257
|
-
hasBodyParam,
|
|
258
|
-
bodyParam: 'body',
|
|
259
|
-
hasOptions: hasQueryParams || hasBodyParam,
|
|
260
|
-
hasBothParamsAndBody: hasQueryParams && hasBodyParam,
|
|
261
|
-
returnType: returnType !== 'void' ? returnType : false,
|
|
262
|
-
returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
|
|
263
|
-
returnTypeVarName: returnType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnType) : false,
|
|
264
|
-
returnBaseTypeVarName: returnBaseType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnBaseType) : false,
|
|
265
|
-
isListContainer: isListContainer,
|
|
266
|
-
vendorExtensions: {}
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
if (Object.keys(selectionFilter).length > 0) {
|
|
272
|
-
Object.keys(tagsMap).forEach((tag) => {
|
|
273
|
-
if (!selectionFilter[tag]) {
|
|
274
|
-
delete tagsMap[tag];
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname));
|
|
278
|
-
if (tagsMap[tag].length === 0)
|
|
279
|
-
delete tagsMap[tag];
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
352
|
// Generate per tag
|
|
284
353
|
Object.keys(tagsMap).forEach((tag) => {
|
|
354
|
+
const tagFilename = (0, name_formatter_1.toCamelCase)(tag);
|
|
355
|
+
const tagFolderPath = baseName ? `${baseName}/${tagFilename}` : tagFilename;
|
|
285
356
|
const returnImports = [];
|
|
286
357
|
const paramImports = [];
|
|
287
358
|
Object.keys(schemas).forEach((s) => {
|
|
288
359
|
const usedAsReturn = tagsMap[tag].some((op) => op.returnType === s || op.returnType === `${s}[]`);
|
|
289
360
|
const usedAsParam = tagsMap[tag].some((op) => op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`));
|
|
290
|
-
const
|
|
361
|
+
const schemaTag = schemaTagMap[s] || 'shared';
|
|
362
|
+
const entry = {
|
|
363
|
+
classname: s,
|
|
364
|
+
classFilename: (0, name_formatter_1.toCamelCase)(s),
|
|
365
|
+
classVarName: (0, name_formatter_1.toCamelCase)(s),
|
|
366
|
+
tagFilename: schemaTag,
|
|
367
|
+
tagFolderPath: baseName ? `${baseName}/${schemaTag}` : schemaTag
|
|
368
|
+
};
|
|
291
369
|
if (usedAsReturn) {
|
|
292
370
|
returnImports.push(entry);
|
|
293
371
|
}
|
|
@@ -301,39 +379,36 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
301
379
|
apis: [
|
|
302
380
|
{
|
|
303
381
|
operations: {
|
|
304
|
-
classname: tag,
|
|
305
|
-
classFilename:
|
|
306
|
-
classVarName:
|
|
382
|
+
classname: (0, name_formatter_1.toPascalCase)(tag),
|
|
383
|
+
classFilename: tagFilename,
|
|
384
|
+
classVarName: tagFilename,
|
|
385
|
+
tagFolderPath,
|
|
307
386
|
constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
|
|
308
387
|
operation: tagsMap[tag],
|
|
309
|
-
// All entity imports (return + param) — for contracts and use-cases
|
|
310
388
|
imports: [...returnImports, ...paramImports],
|
|
311
|
-
// Return-type-only imports — for repo impl (Dto + Entity + Mapper)
|
|
312
389
|
returnImports,
|
|
313
|
-
// Param-only imports — for repo impl (Entity only, no Dto/Mapper)
|
|
314
390
|
paramImports,
|
|
315
|
-
// Environment API key for the repository base URL (e.g. "aprovalmApi")
|
|
316
391
|
environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
|
|
317
392
|
}
|
|
318
393
|
}
|
|
319
394
|
]
|
|
320
395
|
}
|
|
321
396
|
};
|
|
322
|
-
renderTemplate(templatesDir, 'api.use-cases.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${
|
|
323
|
-
renderTemplate(templatesDir, 'api.use-cases.impl.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${
|
|
324
|
-
renderTemplate(templatesDir, 'api.repository.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/repositories', `${
|
|
325
|
-
renderTemplate(templatesDir, 'api.repository.impl.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${
|
|
326
|
-
renderTemplate(templatesDir, 'use-cases.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', `${
|
|
327
|
-
renderTemplate(templatesDir, 'repository.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', `${
|
|
328
|
-
// Mocks
|
|
329
|
-
renderTemplate(templatesDir, 'api.repository.impl.mock.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${
|
|
330
|
-
renderTemplate(templatesDir, 'api.use-cases.mock.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${
|
|
331
|
-
renderTemplate(templatesDir, 'repository.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', `${
|
|
332
|
-
renderTemplate(templatesDir, 'use-cases.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', `${
|
|
397
|
+
renderTemplate(templatesDir, 'api.use-cases.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFolderPath, `${tagFilename}.use-cases.contract.ts`), generatedCount, 'useCases');
|
|
398
|
+
renderTemplate(templatesDir, 'api.use-cases.impl.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFolderPath, `${tagFilename}.use-cases.impl.ts`), generatedCount, 'useCases');
|
|
399
|
+
renderTemplate(templatesDir, 'api.repository.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/repositories', tagFolderPath, `${tagFilename}.repository.contract.ts`), generatedCount, 'repositories');
|
|
400
|
+
renderTemplate(templatesDir, 'api.repository.impl.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFolderPath, `${tagFilename}.repository.impl.ts`), generatedCount, 'repositories');
|
|
401
|
+
renderTemplate(templatesDir, 'use-cases.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', tagFolderPath, `${tagFilename}.use-cases.provider.ts`), generatedCount, 'providers');
|
|
402
|
+
renderTemplate(templatesDir, 'repository.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', tagFolderPath, `${tagFilename}.repository.provider.ts`), generatedCount, 'providers');
|
|
403
|
+
// Mocks
|
|
404
|
+
renderTemplate(templatesDir, 'api.repository.impl.mock.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFolderPath, `${tagFilename}.repository.impl.mock.ts`), generatedCount, 'mocks');
|
|
405
|
+
renderTemplate(templatesDir, 'api.use-cases.mock.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFolderPath, `${tagFilename}.use-cases.mock.ts`), generatedCount, 'mocks');
|
|
406
|
+
renderTemplate(templatesDir, 'repository.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', tagFolderPath, `${tagFilename}.repository.provider.mock.ts`), generatedCount, 'mocks');
|
|
407
|
+
renderTemplate(templatesDir, 'use-cases.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', tagFolderPath, `${tagFilename}.use-cases.provider.mock.ts`), generatedCount, 'mocks');
|
|
333
408
|
// Repository impl spec
|
|
334
|
-
renderTemplate(templatesDir, 'api.repository.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${
|
|
409
|
+
renderTemplate(templatesDir, 'api.repository.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFolderPath, `${tagFilename}.repository.impl.spec.ts`), generatedCount, 'specs');
|
|
335
410
|
// Use-cases impl spec
|
|
336
|
-
renderTemplate(templatesDir, 'api.use-cases.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${
|
|
411
|
+
renderTemplate(templatesDir, 'api.use-cases.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFolderPath, `${tagFilename}.use-cases.impl.spec.ts`), generatedCount, 'specs');
|
|
337
412
|
});
|
|
338
413
|
(0, logger_1.logSuccess)(`${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers, ${generatedCount.mocks} Mocks, ${generatedCount.specs} Specs generated`);
|
|
339
414
|
return generatedCount;
|
|
@@ -344,6 +419,7 @@ function renderTemplate(templatesDir, templateName, viewData, destPath, counter,
|
|
|
344
419
|
if (fs_extra_1.default.existsSync(templatePath)) {
|
|
345
420
|
const template = fs_extra_1.default.readFileSync(templatePath, 'utf8');
|
|
346
421
|
const output = mustache_1.default.render(template, viewData);
|
|
422
|
+
fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
|
|
347
423
|
fs_extra_1.default.writeFileSync(destPath, output);
|
|
348
424
|
counter[key]++;
|
|
349
425
|
(0, logger_1.logDetail)('generate', `${templateName.replace('.mustache', '')} → ${path_1.default.relative(process.cwd(), destPath)}`);
|
|
@@ -10,6 +10,7 @@ const child_process_1 = require("child_process");
|
|
|
10
10
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const logger_1 = require("../utils/logger");
|
|
13
|
+
const name_formatter_1 = require("../utils/name-formatter");
|
|
13
14
|
/** Invokes `openapi-generator-cli` to generate DTOs into a temporary directory. */
|
|
14
15
|
function generateCode(swaggerFile, templatesDir) {
|
|
15
16
|
(0, logger_1.logStep)('Generating code from OpenAPI spec...');
|
|
@@ -37,8 +38,8 @@ function generateCode(swaggerFile, templatesDir) {
|
|
|
37
38
|
process.exit(1);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
/** Copies the generated DTOs from the temporary directory to the output directory. */
|
|
41
|
-
function organizeFiles(tempDir, outputDir) {
|
|
41
|
+
/** Copies the generated DTOs from the temporary directory to the output directory, organised by tag subfolder. */
|
|
42
|
+
function organizeFiles(tempDir, outputDir, schemaTagMap = {}, baseName = '') {
|
|
42
43
|
(0, logger_1.logStep)('Organising generated DTO files...');
|
|
43
44
|
const sourceDir = path_1.default.join(tempDir, 'model');
|
|
44
45
|
const destDir = path_1.default.join(outputDir, 'data/dtos');
|
|
@@ -47,8 +48,15 @@ function organizeFiles(tempDir, outputDir) {
|
|
|
47
48
|
fs_extra_1.default.ensureDirSync(destDir);
|
|
48
49
|
const files = fs_extra_1.default.readdirSync(sourceDir).filter((file) => file.endsWith('.dto.ts'));
|
|
49
50
|
files.forEach((file) => {
|
|
51
|
+
// file is like "userResponse.dto.ts" → derive PascalCase schema name to look up tag
|
|
52
|
+
const camelName = file.replace('.dto.ts', '');
|
|
53
|
+
const pascalName = (0, name_formatter_1.toPascalCase)(camelName);
|
|
54
|
+
const tagFolder = schemaTagMap[pascalName] || 'shared';
|
|
50
55
|
const sourcePath = path_1.default.join(sourceDir, file);
|
|
51
|
-
const destPath =
|
|
56
|
+
const destPath = baseName
|
|
57
|
+
? path_1.default.join(destDir, baseName, tagFolder, file)
|
|
58
|
+
: path_1.default.join(destDir, tagFolder, file);
|
|
59
|
+
fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
|
|
52
60
|
fs_extra_1.default.copySync(sourcePath, destPath);
|
|
53
61
|
filesMoved++;
|
|
54
62
|
(0, logger_1.logDetail)('dto', `${file} → ${path_1.default.relative(process.cwd(), destPath)}`);
|
|
@@ -57,51 +65,67 @@ function organizeFiles(tempDir, outputDir) {
|
|
|
57
65
|
(0, logger_1.logSuccess)(`${filesMoved} DTOs moved successfully`);
|
|
58
66
|
}
|
|
59
67
|
/** Post-processes the generated DTOs: adds cross-DTO imports and normalises Array<T> → T[]. */
|
|
60
|
-
function addDtoImports(outputDir) {
|
|
68
|
+
function addDtoImports(outputDir, baseName = '') {
|
|
61
69
|
(0, logger_1.logStep)('Post-processing generated DTOs...');
|
|
62
|
-
const dtosDir =
|
|
70
|
+
const dtosDir = baseName
|
|
71
|
+
? path_1.default.join(outputDir, 'data/dtos', baseName)
|
|
72
|
+
: path_1.default.join(outputDir, 'data/dtos');
|
|
63
73
|
if (!fs_extra_1.default.existsSync(dtosDir))
|
|
64
74
|
return;
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
// Collect all .dto.ts files from all subfolders (1 level deep)
|
|
76
|
+
const allFiles = [];
|
|
77
|
+
const entries = fs_extra_1.default.readdirSync(dtosDir);
|
|
78
|
+
entries.forEach((entry) => {
|
|
79
|
+
const entryPath = path_1.default.join(dtosDir, entry);
|
|
80
|
+
if (fs_extra_1.default.statSync(entryPath).isDirectory()) {
|
|
81
|
+
fs_extra_1.default.readdirSync(entryPath)
|
|
82
|
+
.filter((f) => f.endsWith('.dto.ts'))
|
|
83
|
+
.forEach((file) => allFiles.push({ subfolder: entry, file, fullPath: path_1.default.join(entryPath, file) }));
|
|
84
|
+
}
|
|
85
|
+
else if (entry.endsWith('.dto.ts')) {
|
|
86
|
+
allFiles.push({ subfolder: '', file: entry, fullPath: entryPath });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// Build map: ClassName → { subfolder, fileBase }
|
|
67
90
|
const dtoMap = {};
|
|
68
|
-
|
|
69
|
-
const content = fs_extra_1.default.readFileSync(
|
|
91
|
+
allFiles.forEach(({ subfolder, file, fullPath }) => {
|
|
92
|
+
const content = fs_extra_1.default.readFileSync(fullPath, 'utf8');
|
|
70
93
|
const match = content.match(/export interface (\w+)/);
|
|
71
|
-
if (match)
|
|
72
|
-
dtoMap[match[1]] = file.replace('.ts', '');
|
|
73
|
-
}
|
|
94
|
+
if (match)
|
|
95
|
+
dtoMap[match[1]] = { subfolder, fileBase: file.replace('.ts', '') };
|
|
74
96
|
});
|
|
75
97
|
let filesProcessed = 0;
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const originalContent = fs_extra_1.default.readFileSync(filePath, 'utf8');
|
|
98
|
+
allFiles.forEach(({ subfolder, file, fullPath }) => {
|
|
99
|
+
const originalContent = fs_extra_1.default.readFileSync(fullPath, 'utf8');
|
|
79
100
|
let content = originalContent;
|
|
80
101
|
const selfMatch = content.match(/export interface (\w+)/);
|
|
81
102
|
const selfName = selfMatch ? selfMatch[1] : '';
|
|
82
|
-
// Normalize Array<T> → T[] (openapi-generator-cli always outputs Array<T>)
|
|
83
103
|
content = content.replace(/Array<(\w+)>/g, '$1[]');
|
|
84
|
-
// Find all Dto type references in the file body (excluding the interface name itself)
|
|
85
104
|
const references = new Set();
|
|
86
105
|
const typeRegex = /\b(\w+Dto)\b/g;
|
|
87
106
|
let match;
|
|
88
107
|
while ((match = typeRegex.exec(content)) !== null) {
|
|
89
|
-
if (match[1] !== selfName)
|
|
108
|
+
if (match[1] !== selfName)
|
|
90
109
|
references.add(match[1]);
|
|
91
|
-
}
|
|
92
110
|
}
|
|
93
|
-
// Build import lines for each referenced type that exists in the dtoMap
|
|
94
111
|
const imports = [];
|
|
95
112
|
references.forEach((ref) => {
|
|
96
113
|
if (dtoMap[ref]) {
|
|
97
|
-
|
|
114
|
+
const { subfolder: refSubfolder, fileBase: refFileBase } = dtoMap[ref];
|
|
115
|
+
const fromDir = subfolder ? path_1.default.join(dtosDir, subfolder) : dtosDir;
|
|
116
|
+
const toFile = refSubfolder
|
|
117
|
+
? path_1.default.join(dtosDir, refSubfolder, refFileBase)
|
|
118
|
+
: path_1.default.join(dtosDir, refFileBase);
|
|
119
|
+
let relPath = path_1.default.relative(fromDir, toFile).replace(/\\/g, '/');
|
|
120
|
+
if (!relPath.startsWith('.'))
|
|
121
|
+
relPath = './' + relPath;
|
|
122
|
+
imports.push(`import { ${ref} } from '${relPath}';`);
|
|
98
123
|
}
|
|
99
124
|
});
|
|
100
|
-
if (imports.length > 0)
|
|
125
|
+
if (imports.length > 0)
|
|
101
126
|
content = imports.join('\n') + '\n' + content;
|
|
102
|
-
}
|
|
103
127
|
if (content !== originalContent) {
|
|
104
|
-
fs_extra_1.default.writeFileSync(
|
|
128
|
+
fs_extra_1.default.writeFileSync(fullPath, content);
|
|
105
129
|
filesProcessed++;
|
|
106
130
|
(0, logger_1.logDetail)('dto', `Post-processed ${file} (added ${imports.length} import(s))`);
|
|
107
131
|
}
|
|
@@ -7,6 +7,7 @@ exports.generateReport = generateReport;
|
|
|
7
7
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const logger_1 = require("../utils/logger");
|
|
10
|
+
const example_validator_1 = require("../utils/example-validator");
|
|
10
11
|
/** Counts files ending with `.mock.ts` in a directory (returns 0 if directory does not exist). */
|
|
11
12
|
function countMockFiles(dir) {
|
|
12
13
|
try {
|
|
@@ -37,6 +38,7 @@ function generateReport(outputDir, analysis, lintResult) {
|
|
|
37
38
|
})).length;
|
|
38
39
|
return { name: t.name, description: t.description || '', endpoints: endpointCount };
|
|
39
40
|
});
|
|
41
|
+
const exampleMismatches = (0, example_validator_1.getExampleMismatches)();
|
|
40
42
|
const report = {
|
|
41
43
|
timestamp: new Date().toISOString(),
|
|
42
44
|
tags: analysis.tags.length,
|
|
@@ -44,6 +46,10 @@ function generateReport(outputDir, analysis, lintResult) {
|
|
|
44
46
|
tagDetails,
|
|
45
47
|
outputDirectory: outputDir,
|
|
46
48
|
linting: lintResult,
|
|
49
|
+
warnings: {
|
|
50
|
+
exampleMismatches: exampleMismatches.map((m) => ({ ...m })),
|
|
51
|
+
total: exampleMismatches.length
|
|
52
|
+
},
|
|
47
53
|
structure: {
|
|
48
54
|
dtos: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'data/dtos')).length,
|
|
49
55
|
repositories: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'data/repositories')).length,
|