Skip to content

Commit

Permalink
feat(CLI Onboarding): Fetch templates from serverless/examples
Browse files Browse the repository at this point in the history
  • Loading branch information
pgrzesik committed May 17, 2021
1 parent 2aaf7e6 commit e4ea50d
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 25 deletions.
36 changes: 28 additions & 8 deletions lib/cli/interactive-setup/service.js
Expand Up @@ -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' },
];

Expand All @@ -27,6 +38,7 @@ const projectTypeChoice = async () =>
type: 'list',
name: 'projectType',
choices: initializeProjectChoices,
pageSize: 13,
})
).projectType;

Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions lib/utils/downloadTemplateFromRepo.js
Expand Up @@ -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;
Expand All @@ -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(() => {
Expand Down
26 changes: 13 additions & 13 deletions test/unit/lib/cli/interactive-setup/index.test.js
Expand Up @@ -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',
Expand Down
61 changes: 59 additions & 2 deletions test/unit/lib/cli/interactive-setup/service.test.js
Expand Up @@ -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');

Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down

0 comments on commit e4ea50d

Please sign in to comment.