From e4ea50d401628cb22612196b7e9b50c4344dab8a Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 14 May 2021 16:18:17 +0200 Subject: [PATCH] feat(CLI Onboarding): Fetch templates from `serverless/examples` --- lib/cli/interactive-setup/service.js | 36 ++++++++--- lib/utils/downloadTemplateFromRepo.js | 6 +- .../lib/cli/interactive-setup/index.test.js | 26 ++++---- .../lib/cli/interactive-setup/service.test.js | 61 ++++++++++++++++++- 4 files changed, 104 insertions(+), 25 deletions(-) diff --git a/lib/cli/interactive-setup/service.js b/lib/cli/interactive-setup/service.js index 2acd167c345..28dc893e5cf 100644 --- a/lib/cli/interactive-setup/service.js +++ b/lib/cli/interactive-setup/service.js @@ -2,21 +2,32 @@ const { join } = require('path'); const chalk = require('chalk'); -const fs = require('fs'); +const fsp = require('fs').promises; const inquirer = require('@serverless/utils/inquirer'); const resolveConfigurationPath = require('../resolve-configuration-path'); const readConfiguration = require('../../configuration/read'); -const createFromTemplate = require('../../utils/createFromTemplate'); const resolveVariables = require('../../configuration/variables'); const { confirm } = require('./utils'); const createFromLocalTemplate = require('../../utils/create-from-local-template'); const ServerlessError = require('../../serverless-error'); +const { downloadTemplateFromRepo } = require('../../utils/downloadTemplateFromRepo'); const isValidServiceName = RegExp.prototype.test.bind(/^[a-zA-Z][a-zA-Z0-9-]{0,100}$/); const initializeProjectChoices = [ - { name: 'AWS Node.js', value: 'aws-nodejs' }, - { name: 'AWS Python', value: 'aws-python3' }, + { name: 'AWS - Node.js - Empty', value: 'aws-node' }, + { name: 'AWS - Node.js - REST API', value: 'aws-node-rest-api' }, + { name: 'AWS - Node.js - Scheduled Task', value: 'aws-node-scheduled-cron' }, + { name: 'AWS - Node.js - SQS Worker', value: 'aws-node-sqs-worker' }, + { name: 'AWS - Node.js - Express API', value: 'aws-node-express-api' }, + { name: 'AWS - Node.js - Express API with DynamoDB', value: 'aws-node-express-dynamodb-api' }, + + { name: 'AWS - Python - Empty', value: 'aws-python' }, + { name: 'AWS - Python - REST API', value: 'aws-python-rest-api' }, + { name: 'AWS - Python - Scheduled Task', value: 'aws-python-scheduled-cron' }, + { name: 'AWS - Python - SQS Worker', value: 'aws-python-sqs-worker' }, + { name: 'AWS - Python - Flask API', value: 'aws-python-flask-api' }, + { name: 'AWS - Python - Flask API with DynamoDB', value: 'aws-python-flask-dynamodb-api' }, { name: 'Other', value: 'other' }, ]; @@ -27,6 +38,7 @@ const projectTypeChoice = async () => type: 'list', name: 'projectType', choices: initializeProjectChoices, + pageSize: 13, }) ).projectType; @@ -49,7 +61,7 @@ const projectNameInput = async (workingDir) => } try { - await fs.promises.access(join(workingDir, input)); + await fsp.access(join(workingDir, input)); return `Path ${input} is already taken`; } catch { return true; @@ -66,7 +78,7 @@ const resolveProjectNameInput = async (options, workingDir) => { let alreadyTaken = false; try { - await fs.promises.access(join(workingDir, options.name)); + await fsp.access(join(workingDir, options.name)); alreadyTaken = true; } catch { // Pass @@ -127,13 +139,21 @@ module.exports = { } projectName = await resolveProjectNameInput(context.options, workingDir); projectDir = join(workingDir, projectName); - await createFromTemplate(projectType, projectDir); + const templateUrl = `https://github.com/serverless/examples/tree/master/${projectType}`; + try { + process.stdout.write(`\n${chalk.green(`Downloading "${projectType}" template...`)}\n`); + await downloadTemplateFromRepo(templateUrl, projectType, projectName, true); + } catch (err) { + throw new ServerlessError( + 'Could not download template. Ensure that you are using the latest version of Serverless Framework.', + 'TEMPLATE_DOWNLOAD_FAILED' + ); + } } process.stdout.write( `\n${chalk.green(`Project successfully created in '${projectName}' folder.`)}\n` ); - context.serviceDir = projectDir; const configurationPath = await resolveConfigurationPath({ cwd: projectDir, options: {} }); context.configurationFilename = configurationPath.slice(projectDir.length + 1); diff --git a/lib/utils/downloadTemplateFromRepo.js b/lib/utils/downloadTemplateFromRepo.js index 63816ad8d1c..5999a7e7021 100644 --- a/lib/utils/downloadTemplateFromRepo.js +++ b/lib/utils/downloadTemplateFromRepo.js @@ -254,7 +254,7 @@ function parseRepoURL(inputUrl) { * @param {string} [path] * @returns {Promise} */ -function downloadTemplateFromRepo(inputUrl, templateName, downloadPath) { +function downloadTemplateFromRepo(inputUrl, templateName, downloadPath, silent = false) { return parseRepoURL(inputUrl).then((repoInformation) => { let serviceName; let dirName; @@ -280,7 +280,9 @@ function downloadTemplateFromRepo(inputUrl, templateName, downloadPath) { throw new ServerlessError(errorMessage, 'TARGET_FOLDER_ALREADY_EXISTS'); } - log(`Downloading and installing "${serviceName}"...`); + if (!silent) { + log(`Downloading and installing "${serviceName}"...`); + } if (isPlainGitURL(inputUrl)) { return spawn('git', ['clone', inputUrl, downloadServicePath]).then(() => { diff --git a/test/unit/lib/cli/interactive-setup/index.test.js b/test/unit/lib/cli/interactive-setup/index.test.js index 12084579790..24f68f0e5ff 100644 --- a/test/unit/lib/cli/interactive-setup/index.test.js +++ b/test/unit/lib/cli/interactive-setup/index.test.js @@ -7,26 +7,26 @@ const path = require('path'); const isTabCompletionSupported = require('../../../../../lib/utils/tabCompletion/isSupported'); const serverlessPath = path.resolve(__dirname, '../../../../../scripts/serverless.js'); +const templatesPath = path.resolve(__dirname, '../../../../../lib/plugins/create/templates'); describe('test/unit/lib/cli/interactive-setup/index.test.js', () => { it('should configure interactive setup flow', async () => { - const slsProcessPromise = spawn('node', [serverlessPath], { - env: { - ...process.env, - SLS_INTERACTIVE_SETUP_ENABLE: '1', - SLS_INTERACTIVE_SETUP_TEST: '1', - BROWSER: 'none', - }, - }); + const slsProcessPromise = spawn( + 'node', + [serverlessPath, '--template-path', path.join(templatesPath, 'aws-nodejs')], + { + env: { + ...process.env, + SLS_INTERACTIVE_SETUP_ENABLE: '1', + SLS_INTERACTIVE_SETUP_TEST: '1', + BROWSER: 'none', + }, + } + ); const slsProcess = slsProcessPromise.child; let output = ''; const program = [ // service - { - instructionString: 'No project detected. Do you want to create a new one?', - input: 'Y', - }, - { instructionString: 'AWS Node.js' }, { instructionString: 'What do you want to call this project?', input: 'interactive-setup-test', diff --git a/test/unit/lib/cli/interactive-setup/service.test.js b/test/unit/lib/cli/interactive-setup/service.test.js index 2782f5ba3f4..3c097166606 100644 --- a/test/unit/lib/cli/interactive-setup/service.test.js +++ b/test/unit/lib/cli/interactive-setup/service.test.js @@ -5,6 +5,7 @@ const path = require('path'); const sinon = require('sinon'); const configureInquirerStub = require('@serverless/test/configure-inquirer-stub'); const step = require('../../../../../lib/cli/interactive-setup/service'); +const proxyquire = require('proxyquire'); const templatesPath = path.resolve(__dirname, '../../../../../lib/plugins/create/templates'); @@ -53,14 +54,37 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { describe('Create new project', () => { it('Should create project at not existing directory', async () => { + const downloadTemplateFromRepoStub = sinon.stub(); + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', { + '../../utils/downloadTemplateFromRepo': { + downloadTemplateFromRepo: downloadTemplateFromRepoStub.callsFake( + async (templateUrl, projectType, projectName) => { + await fsp.mkdir(projectName); + const serverlessYmlContent = ` + service: service + provider: + name: aws + `; + + await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent); + } + ), + }, + }); + configureInquirerStub(inquirer, { confirm: { shouldCreateNewProject: true }, list: { projectType: 'aws-nodejs' }, input: { projectName: 'test-project' }, }); - await step.run({ options: {} }); + await mockedStep.run({ options: {} }); const stats = await fsp.lstat('test-project/serverless.yml'); expect(stats.isFile()).to.be.true; + expect(downloadTemplateFromRepoStub).to.have.been.calledWith( + 'https://github.com/serverless/examples/tree/master/aws-nodejs', + 'aws-nodejs', + 'test-project' + ); }); it('Should create project at not existing directory from a provided `template-path`', async () => { @@ -73,13 +97,46 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { }); it('Should create project at not existing directory with provided `name`', async () => { + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', { + '../../utils/downloadTemplateFromRepo': { + downloadTemplateFromRepo: sinon + .stub() + .callsFake(async (templateUrl, projectType, projectName) => { + await fsp.mkdir(projectName); + const serverlessYmlContent = ` + service: service + provider: + name: aws + `; + + await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent); + }), + }, + }); configureInquirerStub(inquirer, { list: { projectType: 'aws-nodejs' }, }); - await step.run({ options: { name: 'test-project-from-cli-option' } }); + await mockedStep.run({ options: { name: 'test-project-from-cli-option' } }); const stats = await fsp.lstat('test-project-from-cli-option/serverless.yml'); expect(stats.isFile()).to.be.true; }); + + it('Should throw an error when template cannot be downloaded', async () => { + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', { + '../../utils/downloadTemplateFromRepo': { + downloadTemplateFromRepo: sinon.stub().rejects(), + }, + }); + configureInquirerStub(inquirer, { + confirm: { shouldCreateNewProject: true }, + list: { projectType: 'aws-nodejs' }, + input: { projectName: 'test-error-during-download' }, + }); + await expect(mockedStep.run({ options: {} })).to.be.eventually.rejected.and.have.property( + 'code', + 'TEMPLATE_DOWNLOAD_FAILED' + ); + }); }); it('Should not allow project creation in a directory in which already service is configured', async () => {