mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
270 lines
9.6 KiB
JavaScript
270 lines
9.6 KiB
JavaScript
'use strict';
|
|
|
|
const { join } = require('path');
|
|
const fsp = require('fs').promises;
|
|
const spawn = require('child-process-ext/spawn');
|
|
const promptWithHistory = require('@serverless/utils/inquirer/prompt-with-history');
|
|
const { log, progress } = require('@serverless/utils/log');
|
|
const resolveConfigurationPath = require('../resolve-configuration-path');
|
|
const readConfiguration = require('../../configuration/read');
|
|
const resolveVariables = require('../../configuration/variables');
|
|
const createFromLocalTemplate = require('../../utils/create-from-local-template');
|
|
const npmCommandDeferred = require('../../utils/npm-command-deferred');
|
|
const ServerlessError = require('../../serverless-error');
|
|
const { downloadTemplateFromRepo } = require('../../utils/download-template-from-repo');
|
|
|
|
const isValidServiceName = RegExp.prototype.test.bind(/^[a-zA-Z][a-zA-Z0-9-]{0,100}$/);
|
|
|
|
const initializeProjectChoices = [
|
|
{ name: 'AWS - Node.js - Starter', value: 'aws-node' },
|
|
{ name: 'AWS - Node.js - HTTP API', value: 'aws-node-http-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 - Starter', value: 'aws-python' },
|
|
{ name: 'AWS - Python - HTTP API', value: 'aws-python-http-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' },
|
|
];
|
|
|
|
const projectTypeChoice = async ({ stepHistory }) => {
|
|
const projectType = await promptWithHistory({
|
|
message: 'What do you want to make?',
|
|
type: 'list',
|
|
name: 'projectType',
|
|
choices: initializeProjectChoices,
|
|
pageSize: 13,
|
|
recordRawAnswerInHistory: true,
|
|
stepHistory,
|
|
});
|
|
return 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, projectType, stepHistory) => {
|
|
const projectName = await promptWithHistory({
|
|
message: 'What do you want to call this project?',
|
|
type: 'input',
|
|
name: 'projectName',
|
|
stepHistory,
|
|
default: projectType ? `${projectType}-project` : null,
|
|
validate: async (input) => {
|
|
input = input.trim();
|
|
if (!isValidServiceName(input)) {
|
|
return INVALID_PROJECT_NAME_MESSAGE;
|
|
}
|
|
|
|
try {
|
|
await fsp.access(join(workingDir, input));
|
|
return `Path ${input} is already taken`;
|
|
} catch {
|
|
return true;
|
|
}
|
|
},
|
|
});
|
|
return projectName;
|
|
};
|
|
|
|
const resolveProjectNameInput = async ({ options, workingDir, projectType, stepHistory }) => {
|
|
if (options.name) {
|
|
if (!isValidServiceName(options.name)) {
|
|
throw new ServerlessError(INVALID_PROJECT_NAME_MESSAGE, 'INVALID_PROJECT_NAME');
|
|
}
|
|
|
|
let alreadyTaken = false;
|
|
try {
|
|
await fsp.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, projectType, stepHistory);
|
|
};
|
|
|
|
module.exports = {
|
|
isApplicable(context) {
|
|
const { options, serviceDir } = context;
|
|
const notApplicableOptions = new Set(['name', 'template-path', 'template', 'template-url']);
|
|
if (serviceDir && Object.keys(options).some((key) => notApplicableOptions.has(key))) {
|
|
throw new ServerlessError(
|
|
`Cannot setup a new service when being in context of another service (${[
|
|
...notApplicableOptions,
|
|
]
|
|
.map((opt) => `"--${opt}"`)
|
|
.join(', ')} options cannot be applied)`,
|
|
'NOT_APPLICABLE_SERVICE_OPTIONS'
|
|
);
|
|
}
|
|
|
|
const inServiceDir = Boolean(serviceDir);
|
|
if (inServiceDir) {
|
|
context.inapplicabilityReasonCode = 'IN_SERVICE_DIRECTORY';
|
|
}
|
|
return !inServiceDir;
|
|
},
|
|
async run(context) {
|
|
const workingDir = context.cwd || process.cwd();
|
|
|
|
log.notice('Creating a new serverless project');
|
|
log.notice();
|
|
|
|
// Validate if user did not provide more than one of: `template', 'template-url` and `template-path` options
|
|
const templateOptions = new Set(['template-path', 'template', 'template-url']);
|
|
if (Object.keys(context.options).filter((key) => templateOptions.has(key)).length > 1) {
|
|
throw new ServerlessError(
|
|
`You can provide only one of: ${[...templateOptions]
|
|
.map((opt) => `"--${opt}"`)
|
|
.join(', ')} options`,
|
|
'MULTIPLE_TEMPLATE_OPTIONS_PROVIDED'
|
|
);
|
|
}
|
|
|
|
let projectDir;
|
|
let projectName;
|
|
if (context.options['template-path']) {
|
|
projectName = await resolveProjectNameInput({
|
|
options: context.options,
|
|
workingDir,
|
|
stepHistory: context.stepHistory,
|
|
});
|
|
projectDir = join(workingDir, projectName);
|
|
await createFromLocalTemplate({
|
|
templatePath: context.options['template-path'],
|
|
projectDir,
|
|
projectName,
|
|
});
|
|
} else if (context.options['template-url']) {
|
|
projectName = await resolveProjectNameInput({
|
|
options: context.options,
|
|
workingDir,
|
|
stepHistory: context.stepHistory,
|
|
});
|
|
projectDir = join(workingDir, projectName);
|
|
const templateUrl = context.options['template-url'];
|
|
const downloadProgress = progress.get('template-download');
|
|
downloadProgress.notice(`Downloading template from provided url: ${templateUrl}`);
|
|
try {
|
|
await downloadTemplateFromRepo(templateUrl, null, projectName);
|
|
} catch (err) {
|
|
if (err.constructor.name !== 'ServerlessError') throw err;
|
|
|
|
throw new ServerlessError(
|
|
`Could not download template from provided url. Ensure that the template provided with "--template-url" exists: ${err.message}`,
|
|
'INVALID_TEMPLATE_URL'
|
|
);
|
|
}
|
|
downloadProgress.remove();
|
|
} else {
|
|
let projectType;
|
|
if (context.options.template) {
|
|
projectType = context.options.template;
|
|
} else {
|
|
projectType = await projectTypeChoice(context);
|
|
if (projectType === 'other') {
|
|
log.notice();
|
|
log.notice(
|
|
'Run "serverless create --help" to view available templates and create a new project ' +
|
|
'from one of those templates.'
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
projectName = await resolveProjectNameInput({
|
|
options: context.options,
|
|
workingDir,
|
|
projectType,
|
|
stepHistory: context.stepHistory,
|
|
});
|
|
projectDir = join(workingDir, projectName);
|
|
const templateUrl = `https://github.com/serverless/examples/tree/v3/${projectType}`;
|
|
const downloadProgress = progress.get('template-download');
|
|
downloadProgress.notice(`Downloading "${projectType}" template`);
|
|
try {
|
|
await downloadTemplateFromRepo(templateUrl, projectType, projectName);
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT' && context.options.template) {
|
|
throw new ServerlessError(
|
|
'Could not find provided template. Ensure that the template provided with "--template" exists.',
|
|
'INVALID_TEMPLATE'
|
|
);
|
|
}
|
|
|
|
if (err.constructor.name !== 'ServerlessError') throw err;
|
|
|
|
throw new ServerlessError(
|
|
`Could not download template. Ensure that you are using the latest version of Serverless Framework: ${err.message}`,
|
|
'TEMPLATE_DOWNLOAD_FAILED'
|
|
);
|
|
}
|
|
downloadProgress.remove();
|
|
}
|
|
|
|
let hasPackageJson = false;
|
|
try {
|
|
await fsp.access(join(projectDir, 'package.json'));
|
|
hasPackageJson = true;
|
|
} catch {
|
|
// pass
|
|
}
|
|
|
|
if (hasPackageJson) {
|
|
const installProgress = progress.get('npm-install');
|
|
installProgress.notice(`Installing dependencies with "npm" in "${projectName}" folder`);
|
|
const { command, args } = await npmCommandDeferred;
|
|
try {
|
|
await spawn(command, [...args, 'install'], { cwd: projectDir });
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') {
|
|
log.warning();
|
|
log.warning(
|
|
'Cannot install dependencies as "npm" installation could not be found. Please install npm and run "npm install" in directory of created service.'
|
|
);
|
|
} else {
|
|
throw new ServerlessError(
|
|
`Cannot install dependencies: ${err.message}`,
|
|
'DEPENDENCIES_INSTALL_FAILED'
|
|
);
|
|
}
|
|
}
|
|
installProgress.remove();
|
|
}
|
|
|
|
try {
|
|
// Try to remove `serverless.template.yml` file from created project if its present
|
|
await fsp.unlink(join(projectDir, 'serverless.template.yml'));
|
|
} catch {
|
|
// pass
|
|
}
|
|
|
|
log.notice();
|
|
log.notice.success(`Project successfully created in ${projectName} folder`);
|
|
context.serviceDir = projectDir;
|
|
const configurationPath = await resolveConfigurationPath({ cwd: projectDir, options: {} });
|
|
context.configurationFilename = configurationPath.slice(projectDir.length + 1);
|
|
context.configuration = await readConfiguration(configurationPath);
|
|
await resolveVariables(context);
|
|
},
|
|
configuredQuestions: ['projectType', 'projectName'],
|
|
};
|