Skip to content

Commit 4a61490

Browse files
committed
feat: support custom outDir for OpenAPI generated artifacts
Signed-off-by: warisniz02 <warisniz02@gmail.com>
1 parent 0c7708d commit 4a61490

1 file changed

Lines changed: 136 additions & 8 deletions

File tree

packages/cli/generators/openapi/index.js

Lines changed: 136 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const DATASOURCE = 'datasources';
2222
const SERVICE = 'services';
2323
const g = require('../../lib/globalize');
2424
const json5 = require('json5');
25+
const fs = require('fs');
26+
const {Project, SyntaxKind} = require('ts-morph');
2527

2628
const isWindows = process.platform === 'win32';
2729

@@ -87,11 +89,30 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
8789
type: Boolean,
8890
});
8991

92+
this.option('outDir', {
93+
description: g.f('Custom output directory for generated files.'),
94+
required: false,
95+
default: 'src',
96+
type: String,
97+
});
98+
9099
return super._setupGenerator();
91100
}
92101

93102
setOptions() {
94-
return super.setOptions();
103+
const result = super.setOptions();
104+
if (this.options.config) {
105+
const config =
106+
typeof this.options.config === 'string'
107+
? JSON.parse(this.options.config)
108+
: this.options.config;
109+
if (config.outDir) this.options.outDir = config.outDir;
110+
if (config.url) this.options.url = config.url;
111+
if (config.prefix) this.options.prefix = config.prefix;
112+
if (config.client !== undefined) this.options.client = config.client;
113+
if (config.server !== undefined) this.options.server = config.server;
114+
}
115+
return result;
95116
}
96117

97118
checkLoopBackProject() {
@@ -241,6 +262,11 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
241262
log: this.log,
242263
validate: this.options.validate,
243264
promoteAnonymousSchemas: this.options['promote-anonymous-schemas'],
265+
prefix:
266+
this.options.outDir && this.options.outDir !== 'src'
267+
? ''
268+
: this.options.prefix,
269+
previousPrefix: this.options.previousPrefix || '',
244270
});
245271
debugJson('OpenAPI spec', result.apiSpec);
246272
Object.assign(this, result);
@@ -251,6 +277,13 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
251277

252278
async selectControllers() {
253279
if (this.shouldExit()) return;
280+
this.controllerSpecs = this.controllerSpecs.map(c => {
281+
if (this.options.prefix && c.tag && c.tag.includes(this.options.prefix)) {
282+
const splited = c.tag.split(this.options.prefix);
283+
c.tag = splited.join('');
284+
}
285+
return c;
286+
});
254287
const choices = this.controllerSpecs.map(c => {
255288
const names = [];
256289
if (this.options.server !== false) {
@@ -287,8 +320,14 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
287320
);
288321
this.selectedServices = this.selectedControllers;
289322
this.selectedControllers.forEach(c => {
290-
c.fileName = getControllerFileName(c.tag || c.className);
291-
c.serviceFileName = getServiceFileName(c.tag || c.serviceClassName);
323+
const originalClassName = c.className;
324+
const originalServiceClassName = c.serviceClassName;
325+
326+
c.fileName = getControllerFileName(c.tag || originalClassName);
327+
if (this.options.prefix) {
328+
c.fileName = this.options.prefix.toLowerCase() + '.' + c.fileName;
329+
}
330+
c.serviceFileName = getServiceFileName(c.tag || originalServiceClassName);
292331
});
293332
}
294333

@@ -302,7 +341,10 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
302341
if (debug.enabled) {
303342
debug(`Artifact output filename set to: ${controllerFile}`);
304343
}
305-
const dest = this.destinationPath(`src/controllers/${controllerFile}`);
344+
const outDir = this.options.outDir || 'src';
345+
const dest = this.destinationPath(
346+
`${outDir}/controllers/${controllerFile}`,
347+
);
306348
if (debug.enabled) {
307349
debug('Copying artifact to: %s', dest);
308350
}
@@ -349,7 +391,10 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
349391
if (debug.enabled) {
350392
debug(`Artifact output filename set to: ${dataSourceFile}`);
351393
}
352-
const dest = this.destinationPath(`src/datasources/${dataSourceFile}`);
394+
const outDir = this.options.outDir || 'src';
395+
const dest = this.destinationPath(
396+
`${outDir}/datasources/${dataSourceFile}`,
397+
);
353398
if (debug.enabled) {
354399
debug('Copying artifact to: %s', dest);
355400
}
@@ -372,7 +417,8 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
372417
if (debug.enabled) {
373418
debug(`Artifact output filename set to: ${file}`);
374419
}
375-
const dest = this.destinationPath(`src/services/${file}`);
420+
const outDir = this.options.outDir || 'src';
421+
const dest = this.destinationPath(`${outDir}/services/${file}`);
376422
if (debug.enabled) {
377423
debug('Copying artifact to: %s', dest);
378424
}
@@ -396,7 +442,8 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
396442
if (debug.enabled) {
397443
debug(`Artifact output filename set to: ${modelFile}`);
398444
}
399-
const dest = this.destinationPath(`src/models/${modelFile}`);
445+
const outDir = this.options.outDir || 'src';
446+
const dest = this.destinationPath(`${outDir}/models/${modelFile}`);
400447
if (debug.enabled) {
401448
debug('Copying artifact to: %s', dest);
402449
}
@@ -408,7 +455,8 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
408455
// update index file for models and controllers
409456
async _updateIndex(dir) {
410457
const update = async files => {
411-
const targetDir = this.destinationPath(`src/${dir}`);
458+
const outDir = this.options.outDir || 'src';
459+
const targetDir = this.destinationPath(`${outDir}/${dir}`);
412460
for (const f of files) {
413461
// Check all files being generated to ensure they succeeded
414462
const status = this.conflicter.generationStatus[f];
@@ -490,6 +538,9 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
490538
this._generateServiceProxies();
491539
await this._updateIndex(SERVICE);
492540
}
541+
if (this.options.outDir && this.options.outDir !== 'src') {
542+
await this._updateBootOptions();
543+
}
493544
}
494545

495546
install() {
@@ -525,6 +576,83 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
525576
});
526577
}
527578

579+
async _updateBootOptions() {
580+
const invokedFrom = this.destinationRoot();
581+
const applicationPath = path.join(invokedFrom, 'src', 'application.ts');
582+
if (!fs.existsSync(applicationPath)) return;
583+
584+
const relDir = (this.options.outDir || 'src').replace(/^src[\\\\/]/, '');
585+
const artifactTypes = [
586+
{
587+
name: 'controllers',
588+
dir: `${relDir}/controllers`,
589+
extension: '.controller.js',
590+
},
591+
{
592+
name: 'datasources',
593+
dir: `${relDir}/datasources`,
594+
extension: '.datasource.js',
595+
},
596+
{name: 'models', dir: `${relDir}/models`, extension: '.model.js'},
597+
{name: 'services', dir: `${relDir}/services`, extension: '.service.js'},
598+
];
599+
600+
const project = new Project({});
601+
project.addSourceFilesAtPaths(`${invokedFrom}/src/**/*.ts`);
602+
const applicationFile = project.getSourceFileOrThrow(applicationPath);
603+
const constructor = applicationFile.getClasses()[0].getConstructors()[0];
604+
const bootOptionsObject = constructor
605+
.getDescendantsOfKind(SyntaxKind.BinaryExpression)
606+
.find(expr => expr.getLeft().getText() === 'this.bootOptions')
607+
?.getRight()
608+
.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
609+
this.log('bootOptions found: ' + !!bootOptionsObject);
610+
611+
if (bootOptionsObject) {
612+
for (const artifact of artifactTypes) {
613+
let artifactProperty = bootOptionsObject.getProperty(artifact.name);
614+
if (!artifactProperty) {
615+
bootOptionsObject.addPropertyAssignment({
616+
name: artifact.name,
617+
initializer: `{
618+
dirs: ['${artifact.name}'],
619+
extensions: ['${artifact.extension}'],
620+
nested: true,
621+
}`,
622+
});
623+
artifactProperty = bootOptionsObject.getProperty(artifact.name);
624+
}
625+
if (!artifactProperty) continue;
626+
627+
const artifactObject = artifactProperty.getInitializerIfKindOrThrow(
628+
SyntaxKind.ObjectLiteralExpression,
629+
);
630+
let dirsProperty = artifactObject.getProperty('dirs');
631+
if (!dirsProperty) {
632+
artifactObject.addPropertyAssignment({
633+
name: 'dirs',
634+
initializer: `['${artifact.name}']`,
635+
});
636+
dirsProperty = artifactObject.getProperty('dirs');
637+
}
638+
if (!dirsProperty) continue;
639+
640+
const dirsArray = dirsProperty.getInitializerIfKindOrThrow(
641+
SyntaxKind.ArrayLiteralExpression,
642+
);
643+
const exists = dirsArray.getElements().some(el => {
644+
const literal = el.asKind(SyntaxKind.StringLiteral);
645+
return literal?.getLiteralValue() === artifact.dir;
646+
});
647+
if (!exists) {
648+
dirsArray.addElement(`'${artifact.dir}'`);
649+
}
650+
}
651+
applicationFile.formatText();
652+
applicationFile.saveSync();
653+
}
654+
}
655+
528656
async end() {
529657
await super.end();
530658
if (this.shouldExit()) return;

0 commit comments

Comments
 (0)