From a40da75b80ce43c86c20da94cb55501462233dbd Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Thu, 31 May 2018 23:53:49 -0400 Subject: [PATCH] feat(cli): auto-generate / update index.ts for exports fix #1127 Signed-off-by: Taranveer Virk --- docs/site/todo-tutorial-controller.md | 11 ++-- .../templates/src/controllers/index.ts.ejs | 1 + packages/cli/generators/controller/index.js | 26 +++------ packages/cli/lib/artifact-generator.js | 54 +++++++++++++++++++ packages/cli/lib/update-index.js | 23 ++++++++ packages/cli/test/unit/update-index.unit.js | 47 ++++++++++++++++ 6 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 packages/cli/generators/app/templates/src/controllers/index.ts.ejs create mode 100644 packages/cli/lib/update-index.js create mode 100644 packages/cli/test/unit/update-index.unit.js diff --git a/docs/site/todo-tutorial-controller.md b/docs/site/todo-tutorial-controller.md index 2cf7340b21cd..a4c92cdb061b 100644 --- a/docs/site/todo-tutorial-controller.md +++ b/docs/site/todo-tutorial-controller.md @@ -20,11 +20,14 @@ logic will live_! ### Create your controller -So, let's create a controller to handle our Todo routes. Inside the -`src/controllers` directory create the following two files: +So, let's create a controller to handle our Todo routes. You can create an empty +Controller using the CLI as follows: -- `index.ts` (export helper) -- `todo.controller.ts` +```sh +lb4 controller +? Controller class name: todo +? What kind of controller would you like to generate? Empty Controller +``` In addition to creating the handler functions themselves, we'll also be adding decorators that setup the routing as well as the expected parameters of incoming diff --git a/packages/cli/generators/app/templates/src/controllers/index.ts.ejs b/packages/cli/generators/app/templates/src/controllers/index.ts.ejs new file mode 100644 index 000000000000..e4c8eb527e48 --- /dev/null +++ b/packages/cli/generators/app/templates/src/controllers/index.ts.ejs @@ -0,0 +1 @@ +export * from './ping.controller'; diff --git a/packages/cli/generators/controller/index.js b/packages/cli/generators/controller/index.js index b51db95c7f88..b391f1403122 100644 --- a/packages/cli/generators/controller/index.js +++ b/packages/cli/generators/controller/index.js @@ -9,6 +9,7 @@ const ArtifactGenerator = require('../../lib/artifact-generator'); const debug = require('../../lib/debug')('controller-generator'); const inspect = require('util').inspect; const path = require('path'); +const chalk = require('chalk'); const utils = require('../../lib/utils'); // Exportable constants @@ -35,7 +36,7 @@ module.exports = class ControllerGenerator extends ArtifactGenerator { // XXX(kjdelisle): These should be more extensible to allow custom paths // for each artifact type. - this.artifactInfo.outdir = path.resolve( + this.artifactInfo.outDir = path.resolve( this.artifactInfo.rootDir, 'controllers', ); @@ -185,10 +186,10 @@ module.exports = class ControllerGenerator extends ArtifactGenerator { // all of the templates! if (this.shouldExit()) return false; this.artifactInfo.name = utils.toClassName(this.artifactInfo.name); - this.artifactInfo.filename = + this.artifactInfo.outFile = utils.kebabCase(this.artifactInfo.name) + '.controller.ts'; if (debug.enabled) { - debug(`Artifact filename set to: ${this.artifactInfo.filename}`); + debug(`Artifact output filename set to: ${this.artifactInfo.outFile}`); } // renames the file let template = 'controller-template.ts.ejs'; @@ -204,7 +205,7 @@ module.exports = class ControllerGenerator extends ArtifactGenerator { debug(`Using template at: ${source}`); } const dest = this.destinationPath( - path.join(this.artifactInfo.outdir, this.artifactInfo.filename), + path.join(this.artifactInfo.outDir, this.artifactInfo.outFile), ); if (debug.enabled) { @@ -221,20 +222,7 @@ module.exports = class ControllerGenerator extends ArtifactGenerator { return; } - end() { - super.end(); - if (this.shouldExit()) return false; - // logs a message if there is no file conflict - if ( - this.conflicter.generationStatus[this.artifactInfo.filename] !== 'skip' && - this.conflicter.generationStatus[this.artifactInfo.filename] !== - 'identical' - ) { - this.log(); - this.log( - 'Controller %s is now created in src/controllers/', - this.artifactInfo.name, - ); - } + async end() { + await super.end(); } }; diff --git a/packages/cli/lib/artifact-generator.js b/packages/cli/lib/artifact-generator.js index 94c0b64f8e8e..3db0ab03f588 100644 --- a/packages/cli/lib/artifact-generator.js +++ b/packages/cli/lib/artifact-generator.js @@ -7,6 +7,9 @@ const BaseGenerator = require('./base-generator'); const debug = require('./debug')('artifact-generator'); const utils = require('./utils'); +const updateIndex = require('./update-index'); +const path = require('path'); +const chalk = require('chalk'); const StatusConflicter = utils.StatusConflicter; module.exports = class ArtifactGenerator extends BaseGenerator { @@ -29,6 +32,10 @@ module.exports = class ArtifactGenerator extends BaseGenerator { } this.artifactInfo.name = this.args[0]; this.artifactInfo.defaultName = 'new'; + this.artifactInfo.relPath = path.relative( + this.destinationPath(), + this.artifactInfo.outDir, + ); this.conflicter = new StatusConflicter( this.env.adapter, this.options.force, @@ -102,4 +109,51 @@ module.exports = class ArtifactGenerator extends BaseGenerator { {globOptions: {dot: true}}, ); } + + async end() { + const success = super.end(); + if (!success) return false; + + let generationStatus = true; + // Check all files being generated to ensure they succeeded + Object.entries(this.conflicter.generationStatus).forEach(([key, val]) => { + if (val === 'skip' || val === 'identical') generationStatus = false; + }); + + if (generationStatus) { + /** + * Update the index.ts in this.artifactInfo.outDir. Creates it if it + * doesn't exist. + * this.artifactInfo.outFile is what is exported from the file. + * + * Both those properties must be present for this to happen. Optionally, + * this can be disabled even if the properties are present by setting: + * this.artifactInfo.disableIndexUdpdate = true; + */ + if ( + this.artifactInfo.outDir && + this.artifactInfo.outFile && + !this.artifactInfo.disableIndexUpdate + ) { + await updateIndex(this.artifactInfo.outDir, this.artifactInfo.outFile); + // Output for users + this.log( + chalk.green(' update'), + `${this.artifactInfo.relPath}/index.ts`, + ); + } + + // User Output + this.log(); + this.log( + utils.toClassName(this.artifactInfo.type), + chalk.yellow(this.artifactInfo.name), + 'is now created in', + `${this.artifactInfo.relPath}/`, + ); + this.log(); + } + + return false; + } }; diff --git a/packages/cli/lib/update-index.js b/packages/cli/lib/update-index.js new file mode 100644 index 000000000000..a80dc6580bac --- /dev/null +++ b/packages/cli/lib/update-index.js @@ -0,0 +1,23 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/cli +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +const path = require('path'); +const util = require('util'); +const fs = require('fs'); +const appendFileAsync = util.promisify(fs.appendFile); + +/** + * + * @param {String} dir The directory in which index.ts is to be updated/created + * @param {*} file The new file to be exported from index.ts + */ +module.exports = async function(dir, file) { + const indexFile = path.join(dir, 'index.ts'); + if (!file.endsWith('.ts')) { + throw new Error(`${file} must be a TypeScript (.ts) file`); + } + const content = `export * from './${file.slice(0, -3)}';\n`; + await appendFileAsync(indexFile, content); +}; diff --git a/packages/cli/test/unit/update-index.unit.js b/packages/cli/test/unit/update-index.unit.js new file mode 100644 index 000000000000..b610e0f406e6 --- /dev/null +++ b/packages/cli/test/unit/update-index.unit.js @@ -0,0 +1,47 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/cli +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +const updateIndex = require('../../lib/update-index'); +const assert = require('yeoman-assert'); +const path = require('path'); +const util = require('util'); +const fs = require('fs'); +const writeFileAsync = util.promisify(fs.writeFile); + +const testlab = require('@loopback/testlab'); +const expect = testlab.expect; +const TestSandbox = testlab.TestSandbox; + +// Test Sandbox +const SANDBOX_PATH = path.resolve(__dirname, '.sandbox'); +const sandbox = new TestSandbox(SANDBOX_PATH); +const expectedFile = path.join(SANDBOX_PATH, 'index.ts'); + +describe('update-index unit tests', () => { + beforeEach('reset sandbox', () => sandbox.reset()); + + it('creates index.ts when not present', async () => { + await updateIndex(SANDBOX_PATH, 'test.ts'); + assert.file(expectedFile); + assert.fileContent(expectedFile, /export \* from '.\/test';/); + }); + + it('appends to existing index.ts when present', async () => { + await writeFileAsync( + path.join(SANDBOX_PATH, 'index.ts'), + `export * from './first';\n`, + ); + await updateIndex(SANDBOX_PATH, 'test.ts'); + assert.file(expectedFile); + assert.fileContent(expectedFile, /export \* from '.\/first'/); + assert.fileContent(expectedFile, /export \* from '.\/test'/); + }); + + it('throws an error when given a non-ts file', async () => { + expect(updateIndex(SANDBOX_PATH, 'test.js')).to.be.rejectedWith( + /test.js must be a TypeScript \(.ts\) file/, + ); + }); +});