'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, options }) => { const projectType = await promptWithHistory({ message: 'What do you want to make?', type: 'list', name: 'projectType', choices: initializeProjectChoices.filter( // Temporarily filter out Python and SQS projects when console support is intended ({ value }) => !options.console || (!value.includes('python') && !value.includes('sqs')) ), 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'], };