Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/cli/.yo-rc.json
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,14 @@
"name": "promote-anonymous-schemas",
"hide": false
},
"outDir": {
"description": "Custom output directory for generated files.",
"required": false,
"default": "src",
"type": "String",
"name": "outDir",
"hide": false
},
"config": {
"type": "String",
"alias": "c",
Expand Down
170 changes: 161 additions & 9 deletions packages/cli/generators/openapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const DATASOURCE = 'datasources';
const SERVICE = 'services';
const g = require('../../lib/globalize');
const json5 = require('json5');
const fs = require('fs');
const {Project, SyntaxKind} = require('ts-morph');

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

Expand Down Expand Up @@ -87,11 +89,30 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
type: Boolean,
});

this.option('outDir', {
description: g.f('Custom output directory for generated files.'),
required: false,
default: 'src',
type: String,
});

return super._setupGenerator();
}

setOptions() {
return super.setOptions();
const result = super.setOptions();
if (this.options.config) {
const config =
typeof this.options.config === 'string'
? JSON.parse(this.options.config)
: this.options.config;
if (config.outDir) this.options.outDir = config.outDir;
if (config.url) this.options.url = config.url;
if (config.prefix) this.options.prefix = config.prefix;
if (config.client !== undefined) this.options.client = config.client;
if (config.server !== undefined) this.options.server = config.server;
}
return result;
}

checkLoopBackProject() {
Expand Down Expand Up @@ -241,6 +262,11 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
log: this.log,
validate: this.options.validate,
promoteAnonymousSchemas: this.options['promote-anonymous-schemas'],
prefix:
this.options.outDir && this.options.outDir !== 'src'
? ''
: this.options.prefix,
previousPrefix: this.options.previousPrefix || '',
});
debugJson('OpenAPI spec', result.apiSpec);
Object.assign(this, result);
Expand All @@ -251,6 +277,13 @@ module.exports = class OpenApiGenerator extends BaseGenerator {

async selectControllers() {
if (this.shouldExit()) return;
this.controllerSpecs = this.controllerSpecs.map(c => {
if (this.options.prefix && c.tag && c.tag.includes(this.options.prefix)) {
const splited = c.tag.split(this.options.prefix);
c.tag = splited.join('');
}
return c;
});
const choices = this.controllerSpecs.map(c => {
const names = [];
if (this.options.server !== false) {
Expand Down Expand Up @@ -287,8 +320,17 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
);
this.selectedServices = this.selectedControllers;
this.selectedControllers.forEach(c => {
c.fileName = getControllerFileName(c.tag || c.className);
c.serviceFileName = getServiceFileName(c.tag || c.serviceClassName);
const originalClassName = c.className;
const originalServiceClassName = c.serviceClassName;

c.fileName = getControllerFileName(c.tag || originalClassName);
if (
this.options.prefix &&
(!this.options.outDir || this.options.outDir === 'src')
) {
c.fileName = this.options.prefix.toLowerCase() + '.' + c.fileName;
}
c.serviceFileName = getServiceFileName(c.tag || originalServiceClassName);
});
}

Expand All @@ -302,7 +344,10 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
if (debug.enabled) {
debug(`Artifact output filename set to: ${controllerFile}`);
}
const dest = this.destinationPath(`src/controllers/${controllerFile}`);
const outDir = this.options.outDir || 'src';
const dest = this.destinationPath(
`${outDir}/controllers/${controllerFile}`,
);
if (debug.enabled) {
debug('Copying artifact to: %s', dest);
}
Expand Down Expand Up @@ -349,7 +394,10 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
if (debug.enabled) {
debug(`Artifact output filename set to: ${dataSourceFile}`);
}
const dest = this.destinationPath(`src/datasources/${dataSourceFile}`);
const outDir = this.options.outDir || 'src';
const dest = this.destinationPath(
`${outDir}/datasources/${dataSourceFile}`,
);
if (debug.enabled) {
debug('Copying artifact to: %s', dest);
}
Expand All @@ -372,7 +420,8 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
if (debug.enabled) {
debug(`Artifact output filename set to: ${file}`);
}
const dest = this.destinationPath(`src/services/${file}`);
const outDir = this.options.outDir || 'src';
const dest = this.destinationPath(`${outDir}/services/${file}`);
if (debug.enabled) {
debug('Copying artifact to: %s', dest);
}
Expand All @@ -396,19 +445,42 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
if (debug.enabled) {
debug(`Artifact output filename set to: ${modelFile}`);
}
const dest = this.destinationPath(`src/models/${modelFile}`);
const outDir = this.options.outDir || 'src';
const dest = this.destinationPath(`${outDir}/models/${modelFile}`);
if (debug.enabled) {
debug('Copying artifact to: %s', dest);
}
const source = m.kind === 'class' ? modelSource : typeSource;
let source = m.kind === 'class' ? modelSource : typeSource;
if (
m.kind === 'class' &&
modelFile.includes('-with-relations.model.ts')
) {
const modelSourceWithExportsOnly = this.templatePath(
'src/models/model-template-with-re-exports-only.ts.ejs',
);
source = modelSourceWithExportsOnly;
if (this.options.prefix) m.prefix = this.options.prefix.toLowerCase();
const exportModelFileName = modelFile.split(
'-with-relations.model.ts',
)[0];
let modelName = exportModelFileName;
if (m.prefix) {
const stripped = exportModelFileName.split(m.prefix + '-')[1];
modelName = stripped ?? exportModelFileName;
}
m.exportModelName = utils.toClassName(modelName);
m.exportModelFileName = exportModelFileName + '.model';
if (m.prefix) m.prefix = utils.toClassName(m.prefix);
}
this.copyTemplatedFiles(source, dest, mixinEscapeComment(m));
}
}

// update index file for models and controllers
async _updateIndex(dir) {
const update = async files => {
const targetDir = this.destinationPath(`src/${dir}`);
const outDir = this.options.outDir || 'src';
const targetDir = this.destinationPath(`${outDir}/${dir}`);
for (const f of files) {
// Check all files being generated to ensure they succeeded
const status = this.conflicter.generationStatus[f];
Expand Down Expand Up @@ -490,6 +562,9 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
this._generateServiceProxies();
await this._updateIndex(SERVICE);
}
if (this.options.outDir && this.options.outDir !== 'src') {
await this._updateBootOptions();
}
}

install() {
Expand Down Expand Up @@ -525,6 +600,83 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
});
}

async _updateBootOptions() {
const invokedFrom = this.destinationRoot();
const applicationPath = path.join(invokedFrom, 'src', 'application.ts');
if (!fs.existsSync(applicationPath)) return;

const relDir = (this.options.outDir || 'src').replace(/^src[\\\\/]/, '');
const artifactTypes = [
{
name: 'controllers',
dir: `${relDir}/controllers`,
extension: '.controller.js',
},
{
name: 'datasources',
dir: `${relDir}/datasources`,
extension: '.datasource.js',
},
{name: 'models', dir: `${relDir}/models`, extension: '.model.js'},
{name: 'services', dir: `${relDir}/services`, extension: '.service.js'},
];

const project = new Project({});
project.addSourceFilesAtPaths(`${invokedFrom}/src/**/*.ts`);
const applicationFile = project.getSourceFileOrThrow(applicationPath);
const constructor = applicationFile.getClasses()[0].getConstructors()[0];
const bootOptionsObject = constructor
.getDescendantsOfKind(SyntaxKind.BinaryExpression)
.find(expr => expr.getLeft().getText() === 'this.bootOptions')
?.getRight()
.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
this.log('bootOptions found: ' + !!bootOptionsObject);

if (bootOptionsObject) {
for (const artifact of artifactTypes) {
let artifactProperty = bootOptionsObject.getProperty(artifact.name);
if (!artifactProperty) {
bootOptionsObject.addPropertyAssignment({
name: artifact.name,
initializer: `{
dirs: ['${artifact.name}'],
extensions: ['${artifact.extension}'],
nested: true,
}`,
});
artifactProperty = bootOptionsObject.getProperty(artifact.name);
}
if (!artifactProperty) continue;

const artifactObject = artifactProperty.getInitializerIfKindOrThrow(
SyntaxKind.ObjectLiteralExpression,
);
let dirsProperty = artifactObject.getProperty('dirs');
if (!dirsProperty) {
artifactObject.addPropertyAssignment({
name: 'dirs',
initializer: `['${artifact.name}']`,
});
dirsProperty = artifactObject.getProperty('dirs');
}
if (!dirsProperty) continue;

const dirsArray = dirsProperty.getInitializerIfKindOrThrow(
SyntaxKind.ArrayLiteralExpression,
);
const exists = dirsArray.getElements().some(el => {
const literal = el.asKind(SyntaxKind.StringLiteral);
return literal?.getLiteralValue() === artifact.dir;
});
if (!exists) {
dirsArray.addElement(`'${artifact.dir}'`);
}
}
applicationFile.formatText();
applicationFile.saveSync();
}
}

async end() {
await super.end();
if (this.shouldExit()) return;
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/test/fixtures/copyright/single-package/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright IBM Corp. and LoopBack contributors 2020. All Rights Reserved.
// Node module: @loopback/cli
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
// Copyright ACME Inc. 2020,2026. All Rights Reserved.
// Node module: myapp
// This file is licensed under the ISC License.
// License text available at https://www.isc.org/licenses/

// Use the same set of files (except index.js) in this folder
const files = require('../index')(__dirname);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
// Copyright ACME Inc. 2020,2026. All Rights Reserved.
// Node module: myapp
// This file is licensed under the ISC License.
// License text available at https://www.isc.org/licenses/

// XYZ
exports.xyz = {};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Copyright ACME Inc. 2020,2026. All Rights Reserved.
// Node module: myapp
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
// This file is licensed under the ISC License.
// License text available at https://www.isc.org/licenses/

export class MyApplication {}
Loading
Loading