Skip to content

Commit

Permalink
feat(CLI Onboarding): Support --name CLI option
Browse files Browse the repository at this point in the history
  • Loading branch information
pgrzesik committed May 14, 2021
1 parent 98c9700 commit 53575dc
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 27 deletions.
3 changes: 3 additions & 0 deletions lib/cli/commands-schema/no-service.js
Expand Up @@ -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'],
});
Expand Down
50 changes: 39 additions & 11 deletions lib/cli/interactive-setup/service.js
Expand Up @@ -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}$/);

Expand All @@ -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({
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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);
}
Expand Down
15 changes: 11 additions & 4 deletions lib/utils/create-from-local-template.js
@@ -1,24 +1,31 @@
'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(
`Could not find a template under provider path: ${sourcePath}`,
'INVALID_TEMPLATE_PATH'
);
}
throw err;

throw new ServerlessError(`Cannot copy template: ${err.message}`, 'COPY_LOCAL_TEMPLATE_ERROR');
}

if (projectName) {
Expand Down
43 changes: 38 additions & 5 deletions test/unit/lib/cli/interactive-setup/service.test.js
Expand Up @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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;
});
Expand All @@ -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 () => {
Expand All @@ -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');
});
});
12 changes: 5 additions & 7 deletions test/unit/lib/utils/create-from-local-template.test.js
Expand Up @@ -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,
});
Expand All @@ -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',
Expand Down

0 comments on commit 53575dc

Please sign in to comment.