From 53575dc36017ded5ff60e5edf18fb7a9fd9d30e9 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Thu, 13 May 2021 10:56:04 +0200 Subject: [PATCH] feat(CLI Onboarding): Support `--name` CLI option --- lib/cli/commands-schema/no-service.js | 3 ++ lib/cli/interactive-setup/service.js | 50 +++++++++++++++---- lib/utils/create-from-local-template.js | 15 ++++-- .../lib/cli/interactive-setup/service.test.js | 43 ++++++++++++++-- .../utils/create-from-local-template.test.js | 12 ++--- 5 files changed, 96 insertions(+), 27 deletions(-) diff --git a/lib/cli/commands-schema/no-service.js b/lib/cli/commands-schema/no-service.js index 81d54ad52c1..4945170cd11 100644 --- a/lib/cli/commands-schema/no-service.js +++ b/lib/cli/commands-schema/no-service.js @@ -15,6 +15,9 @@ commands.set('', { 'template-path': { usage: 'Template local path for the service.', }, + 'name': { + usage: 'Name for the service.', + }, }, lifecycleEvents: ['initializeService', 'setupAws', 'autoUpdate', 'tabCompletion', 'end'], }); diff --git a/lib/cli/interactive-setup/service.js b/lib/cli/interactive-setup/service.js index 34f2a4403dc..7b89dd835a8 100644 --- a/lib/cli/interactive-setup/service.js +++ b/lib/cli/interactive-setup/service.js @@ -10,6 +10,7 @@ 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 isValidServiceName = RegExp.prototype.test.bind(/^[a-zA-Z][a-zA-Z0-9-]{0,100}$/); @@ -29,6 +30,12 @@ const projectTypeChoice = async () => }) ).projectType; +const INVALID_PROJECT_NAME_MESSAGE = + 'Project name is not valid.\n' + + ' - It should only contain alphanumeric and hyphens.\n' + + ' - It should start with an alphabetic character.\n' + + " - Shouldn't exceed 128 characters"; + const projectNameInput = async (workingDir) => ( await inquirer.prompt({ @@ -38,12 +45,7 @@ const projectNameInput = async (workingDir) => validate: async (input) => { input = input.trim(); if (!isValidServiceName(input)) { - return ( - 'Project name is not valid.\n' + - ' - It should only contain alphanumeric and hyphens.\n' + - ' - It should start with an alphabetic character.\n' + - " - Shouldn't exceed 128 characters" - ); + return INVALID_PROJECT_NAME_MESSAGE; } try { @@ -56,6 +58,33 @@ const projectNameInput = async (workingDir) => }) ).projectName.trim(); +const resolveProjectNameInput = async (options, workingDir) => { + if (options.name) { + if (!isValidServiceName(options.name)) { + throw new ServerlessError(INVALID_PROJECT_NAME_MESSAGE, 'INVALID_PROJECT_NAME'); + } + + let alreadyTaken = false; + try { + await fs.promises.access(join(workingDir, options.name)); + alreadyTaken = true; + } catch { + // Pass + } + + if (alreadyTaken) { + throw new ServerlessError( + `Path ${options.name} is already taken`, + 'TARGET_FOLDER_ALREADY_EXISTS' + ); + } + + return options.name; + } + + return projectNameInput(workingDir); +}; + module.exports = { isApplicable({ serviceDir }) { return !serviceDir; @@ -68,12 +97,11 @@ module.exports = { if (!isConfirmed) return; let projectDir; - // TOOD: CLEANUP let projectName; - if (context.options && context.options['template-path']) { - projectName = await projectNameInput(workingDir); + if (context.options['template-path']) { + projectName = await resolveProjectNameInput(context.options, workingDir); projectDir = join(workingDir, projectName); - createFromLocalTemplate({ + await createFromLocalTemplate({ templatePath: context.options['template-path'], projectDir, projectName, @@ -87,7 +115,7 @@ module.exports = { ); return; } - projectName = await projectNameInput(workingDir); + projectName = await resolveProjectNameInput(context.options, workingDir); projectDir = join(workingDir, projectName); await createFromTemplate(projectType, projectDir); } diff --git a/lib/utils/create-from-local-template.js b/lib/utils/create-from-local-template.js index ac4eb9e0387..473661f9cc4 100644 --- a/lib/utils/create-from-local-template.js +++ b/lib/utils/create-from-local-template.js @@ -1,16 +1,22 @@ 'use strict'; -const copyDirContentsSync = require('./fs/copyDirContentsSync'); const untildify = require('untildify'); +const fse = require('fs-extra'); const { renameService } = require('./renameService'); const ServerlessError = require('../serverless-error'); -module.exports = ({ templatePath, projectDir, projectName }) => { +module.exports = async ({ templatePath, projectDir, projectName }) => { const sourcePath = untildify(templatePath); try { - copyDirContentsSync(sourcePath, projectDir, { noLinks: true }); + await fse.copy(sourcePath, projectDir, { + dereference: true, + filter: async (src) => { + const stats = await fse.lstat(src); + return !stats.isSymbolicLink(); + }, + }); } catch (err) { if (err.code === 'ENOENT') { throw new ServerlessError( @@ -18,7 +24,8 @@ module.exports = ({ templatePath, projectDir, projectName }) => { 'INVALID_TEMPLATE_PATH' ); } - throw err; + + throw new ServerlessError(`Cannot copy template: ${err.message}`, 'COPY_LOCAL_TEMPLATE_ERROR'); } if (projectName) { diff --git a/test/unit/lib/cli/interactive-setup/service.test.js b/test/unit/lib/cli/interactive-setup/service.test.js index c4f73617b7c..089b1e643e4 100644 --- a/test/unit/lib/cli/interactive-setup/service.test.js +++ b/test/unit/lib/cli/interactive-setup/service.test.js @@ -30,7 +30,7 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { configureInquirerStub(inquirer, { confirm: { shouldCreateNewProject: false }, }); - await step.run({}); + await step.run({ options: {} }); return confirmEmptyWorkingDir(); }); @@ -39,7 +39,7 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { confirm: { shouldCreateNewProject: true }, list: { projectType: 'other' }, }); - await step.run({}); + await step.run({ options: {} }); return confirmEmptyWorkingDir(); }); @@ -50,7 +50,7 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { list: { projectType: 'aws-nodejs' }, input: { projectName: 'test-project' }, }); - await step.run({}); + await step.run({ options: {} }); const stats = await fsp.lstat('test-project/serverless.yml'); expect(stats.isFile()).to.be.true; }); @@ -64,6 +64,16 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { const stats = await fsp.lstat('test-project-from-local-template/serverless.yml'); expect(stats.isFile()).to.be.true; }); + + it('Should create project at not existing directory with provided `name`', async () => { + configureInquirerStub(inquirer, { + confirm: { shouldCreateNewProject: true }, + list: { projectType: 'aws-nodejs' }, + }); + await step.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 not allow project creation in a directory in which already service is configured', async () => { @@ -75,21 +85,44 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { await fsp.mkdir('existing'); - await expect(step.run({})).to.eventually.be.rejected.and.have.property( + await expect(step.run({ options: {} })).to.eventually.be.rejected.and.have.property( 'code', 'INVALID_ANSWER' ); }); + it('Should not allow project creation in a directory in which already service is configured when `name` flag provided', async () => { + configureInquirerStub(inquirer, { + confirm: { shouldCreateNewProject: true }, + list: { projectType: 'aws-nodejs' }, + }); + + await fsp.mkdir('anotherexisting'); + + await expect( + step.run({ options: { name: 'anotherexisting' } }) + ).to.eventually.be.rejected.and.have.property('code', 'TARGET_FOLDER_ALREADY_EXISTS'); + }); + it('Should not allow project creation using an invalid project name', async () => { configureInquirerStub(inquirer, { confirm: { shouldCreateNewProject: true }, list: { projectType: 'aws-nodejs' }, input: { projectName: 'elo grzegżółka' }, }); - await expect(step.run({})).to.eventually.be.rejected.and.have.property( + await expect(step.run({ options: {} })).to.eventually.be.rejected.and.have.property( 'code', 'INVALID_ANSWER' ); }); + + it('Should not allow project creation using an invalid project name when `name` flag provided', async () => { + configureInquirerStub(inquirer, { + confirm: { shouldCreateNewProject: true }, + list: { projectType: 'aws-nodejs' }, + }); + await expect( + step.run({ options: { name: 'elo grzegżółka' } }) + ).to.eventually.be.rejected.and.have.property('code', 'INVALID_PROJECT_NAME'); + }); }); diff --git a/test/unit/lib/utils/create-from-local-template.test.js b/test/unit/lib/utils/create-from-local-template.test.js index f37acab3b57..4abe6c35f5e 100644 --- a/test/unit/lib/utils/create-from-local-template.test.js +++ b/test/unit/lib/utils/create-from-local-template.test.js @@ -17,7 +17,7 @@ describe('test/unit/lib/utils/create-from-local-template.test.js', () => { describe('Without `projectName` provided', () => { it('should create from template referenced locally', async () => { const tmpDirPath = path.join(getTmpDirPath(), 'some-service'); - createFromLocalTemplate({ + await createFromLocalTemplate({ templatePath: path.join(templatesPath, 'aws-nodejs'), projectDir: tmpDirPath, }); @@ -29,23 +29,21 @@ describe('test/unit/lib/utils/create-from-local-template.test.js', () => { describe('When `templatePath` does not exist', () => { it('should result in an error', async () => { const tmpDirPath = path.join(getTmpDirPath(), 'some-service'); - expect(() => + await expect( createFromLocalTemplate({ templatePath: path.join(templatesPath, 'nonexistent'), projectDir: tmpDirPath, }) - ) - .to.throw() - .and.have.property('code', 'INVALID_TEMPLATE_PATH'); + ).to.eventually.be.rejected.and.have.property('code', 'INVALID_TEMPLATE_PATH'); }); }); describe('With `projectName` provided', () => { let tmpDirPath; - before(() => { + before(async () => { tmpDirPath = path.join(getTmpDirPath(), 'some-service'); - createFromLocalTemplate({ + await createFromLocalTemplate({ templatePath: path.join(templatesPath, 'fn-nodejs'), projectDir: tmpDirPath, projectName: 'testproj',