diff --git a/lib/classes/plugin-manager.js b/lib/classes/plugin-manager.js index b11cbf0a0..361dc6300 100644 --- a/lib/classes/plugin-manager.js +++ b/lib/classes/plugin-manager.js @@ -5,7 +5,6 @@ const _ = require('lodash'); const ServerlessError = require('../serverless-error'); const resolveCliInput = require('../cli/resolve-input'); const renderCommandHelp = require('../cli/render-help/command'); -const processBackendNotificationRequest = require('../utils/process-backend-notification-request'); const tokenizeException = require('../utils/tokenize-exception'); const getRequire = require('../utils/get-require'); const importModule = require('../utils/require-with-import-fallback'); @@ -380,6 +379,16 @@ class PluginManager { } } + /** + * Retrieves all public commands and aliases from the plugin manager. This method + * iterates through the commands and stops at entrypoints to include only public + * commands throughout the hierarchy. It then iterates through the existing aliases + * and adds them as commands. The result is an object that maps command names to + * their respective command objects, with each command object omitting its own + * 'commands' property to prevent circular references. + * + * @returns {Object} An object mapping command names to command objects. + */ getCommands() { const result = {}; @@ -476,10 +485,12 @@ class PluginManager { // Invalid command, can happen only when Framework is used programmatically, // as otherwise command is validated in main script - throw new ServerlessError( - `Unrecognized command "${commandsArray.join(' ')}"`, + const err = new ServerlessError( + `Unrecognized command "${commandsArray.join(' ')}".`, 'UNRECOGNIZED COMMAND' ); + err.stack = undefined; + throw err; }, { commands: this.commands } ); @@ -578,7 +589,16 @@ class PluginManager { } /** - * Called by the CLI to start a public command. + * Executes the provided commands array. This method initializes hooks, invokes + * the commands, and handles any exceptions that occur during command execution. + * If an exception occurs, it triggers the error hooks and rethrows the exception. + * After successful execution or error handling, it triggers the finalize hooks. + * If an exception occurs during finalize, it rethrows the exception. + * + * @async + * @param {Array} commandsArray - The array of commands to execute. + * @throws {Error} If there's an error in command execution or during the + * execution of error or finalize hooks. */ async run(commandsArray) { this.commandRunStartTime = Date.now(); @@ -613,10 +633,6 @@ class PluginManager { await deferredBackendNotificationRequest; throw finalizeHookException; } - - if (deferredBackendNotificationRequest) { - await processBackendNotificationRequest(await deferredBackendNotificationRequest); - } } /** diff --git a/lib/classes/service.js b/lib/classes/service.js index 7ac466888..d534089ab 100644 --- a/lib/classes/service.js +++ b/lib/classes/service.js @@ -175,6 +175,16 @@ class Service { return this; } + /** + * Reloads the Service from the initial serverless configuration. + * This method is responsible for setting the provider, package, custom, resources, + * functions, config validation mode, layers (if the provider is AWS), and outputs + * based on the initial serverless configuration. If no stage is specified for the + * provider, it defaults to 'dev'. If no functions or layers are specified, they + * default to empty objects. + * + * @throws {Error} If there's an error in loading the service file parameters. + */ reloadServiceFileParam() { const configurationInput = this.initialServerlessConfig; if (_.isObject(configurationInput.provider)) { @@ -188,9 +198,7 @@ class Service { this.resources = configurationInput.resources; this.functions = configurationInput.functions || {}; this.configValidationMode = configurationInput.configValidationMode || 'warn'; - if (this.provider.name === 'aws') { - this.layers = configurationInput.layers || {}; - } + this.layers = configurationInput.layers || {}; this.outputs = configurationInput.outputs; } @@ -271,13 +279,6 @@ class Service { 'You can safely remove it from the configuration' ); } - if (userConfig.variablesResolutionMode != null) { - this.serverless._logDeprecation( - 'VARIABLES_RESOLUTION_MODE', - 'Starting with v3.0, the "variablesResolutionMode" option is now useless. ' + - 'You can safely remove it from the configuration' - ); - } if (userConfig.console != null) { this.serverless._logDeprecation( 'CONSOLE_CONFIGURATION', diff --git a/lib/cli/commands-options-schema.js b/lib/cli/commands-options-schema.js new file mode 100644 index 000000000..fd8b3b092 --- /dev/null +++ b/lib/cli/commands-options-schema.js @@ -0,0 +1,59 @@ +'use strict'; + +module.exports = { + help: { + usage: 'Show this message', + shortcut: 'h', + type: 'boolean' + }, + version: { + usage: 'Show version info', + shortcut: 'v', + type: 'boolean' + }, + verbose: { + usage: 'Show verbose logs', + type: 'boolean' + }, + debug: { + usage: 'Namespace of debug logs to expose (use "*" to display all)', + type: 'string' + }, + config: { + usage: 'Path to serverless config file', + shortcut: 'c', + type: 'string' + }, + stage: { + usage: 'Stage of the service', + shortcut: 's', + type: 'string' + }, + param: { + usage: 'Pass custom parameter values for "param" variable source (usage: --param="key=value")', + type: 'multiple', + }, + 'region': { + usage: 'Region of the service', + shortcut: 'r', + type: 'string', + }, + 'aws-profile': { + usage: 'AWS profile to use with the command', + type: 'string', + }, + 'app': { + usage: 'Serverless Framework Dashboard App', + type: 'string', + }, + 'org': { + usage: 'Serverless Framework Dashboard Org', + type: 'string', + }, + 'use-local-credentials': { + usage: + 'Rely on locally resolved AWS credentials instead of loading them from ' + + 'Serverless Framework Providers. This applies only to services signed into the Serverless Framework Dashboard.', + type: 'boolean', + }, +}; diff --git a/lib/cli/commands-schema/aws-service.js b/lib/cli/commands-schema.js similarity index 52% rename from lib/cli/commands-schema/aws-service.js rename to lib/cli/commands-schema.js index 1f24d6d7f..ab4c7b002 100644 --- a/lib/cli/commands-schema/aws-service.js +++ b/lib/cli/commands-schema.js @@ -1,11 +1,188 @@ +/** + * Serverless Framework Default Command Schema. + * In earlier versions of the Framework, there were multiple schemas + * for providers and being in or outside of a Service. + * In V.4 we have a single schema for all commands, since + * we only support the AWS provider. + */ + 'use strict'; -const awsServiceOptions = require('./common-options/aws-service'); -const serviceCommands = require('./service'); +const globalOptions = require('./commands-options-schema'); const commands = (module.exports = new Map()); -commands.commonOptions = awsServiceOptions; +commands.commonOptions = globalOptions; + +commands.set('', { + usage: 'Interactive Quickstart', + serviceDependencyMode: 'optional', + hasAwsExtension: true, + options: { + 'help-interactive': { + usage: 'Show this message', + type: 'boolean' + }, + 'name': { + type: 'string', + usage: 'Name for the service.', + }, + 'template': { + type: 'string', + usage: 'Name of template for the service.', + }, + 'template-path': { + type: 'string', + usage: 'Template local path for the service.', + }, + 'template-url': { + type: 'string', + usage: 'Template url for the service.', + }, + 'function': { + usage: 'Name of the function you would like the dev mode activity feed to observe.', + type: 'string', + shortcut: 'f', + }, + }, + lifecycleEvents: [ + 'initializeService', + 'setupAws', + 'autoUpdate', + 'end' + ], +}); + +commands.set('config credentials', { + usage: 'Configures a new provider profile for the Serverless Framework', + hasAwsExtension: true, + options: { + provider: { + type: 'string', + usage: 'Name of the provider. Supported providers: aws', + required: true, + shortcut: 'p', + }, + key: { + type: 'string', + usage: 'Access key for the provider', + shortcut: 'k', + required: true, + }, + secret: { + type: 'string', + usage: 'Secret key for the provider', + shortcut: 's', + required: true, + }, + profile: { + type: 'string', + usage: 'Name of the profile you wish to create. Defaults to "default"', + shortcut: 'n', + }, + overwrite: { + usage: 'Overwrite the existing profile configuration in the credentials file', + shortcut: 'o', + type: 'boolean', + }, + }, + lifecycleEvents: [ + 'config' + ], +}); + +commands.set('config', { + usage: 'Configure Serverless', + options: { + autoupdate: { + usage: 'Turn on auto update mechanism (turn off via "--no-autoupdate")', + type: 'boolean', + }, + }, + lifecycleEvents: [ + 'config' + ], +}); + +commands.set('plugin install', { + usage: 'Install and add a plugin to your service', + options: { + name: { + type: 'string', + usage: 'The plugin name', + required: true, + shortcut: 'n', + }, + }, + lifecycleEvents: [ + 'install' + ], + serviceDependencyMode: 'required' +}); + +commands.set('plugin uninstall', { + usage: 'Uninstall and remove a plugin from your service', + options: { + name: { + type: 'string', + usage: 'The plugin name', + required: true, + shortcut: 'n', + }, + }, + lifecycleEvents: [ + 'uninstall' + ], + serviceDependencyMode: 'required' +}); + +commands.set('print', { + usage: 'Print your compiled and resolved config file', + hasAwsExtension: true, + options: { + format: { + type: 'string', + usage: 'Print configuration in given format ("yaml", "json", "text"). Default: yaml', + }, + path: { + type: 'string', + usage: 'Optional period-separated path to print a sub-value (eg: "provider.name")', + }, + transform: { + type: 'string', + usage: 'Optional transform-function to apply to the value ("keys")', + }, + }, + lifecycleEvents: ['print'], + serviceDependencyMode: 'required' +}); + +commands.set('package', { + usage: 'Packages a Serverless Service', + hasAwsExtension: true, + options: { + 'package': { + type: 'string', + usage: 'Output path for the package', + shortcut: 'p', + }, + 'minify-template': { + usage: 'Minify the AWS CloudFormation template for AWS packages', + type: 'boolean', + }, + }, + lifecycleEvents: [ + 'cleanup', + 'initialize', + 'setupProviderConfiguration', + 'createDeploymentArtifacts', + 'compileLayers', + 'compileFunctions', + 'compileEvents', + 'finalize', + ], + serviceDependencyMode: 'required', +}); commands.set('deploy', { groupName: 'main', @@ -16,6 +193,7 @@ commands.set('deploy', { type: 'boolean', }, 'package': { + type: 'string', usage: 'Path of the deployment package', shortcut: 'p', }, @@ -38,6 +216,8 @@ commands.set('deploy', { }, }, lifecycleEvents: ['deploy', 'finalize'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('deploy function', { @@ -45,6 +225,7 @@ commands.set('deploy function', { usage: 'Deploy a single function from the service', options: { 'function': { + type: 'string', usage: 'Name of the function', shortcut: 'f', required: true, @@ -60,15 +241,24 @@ commands.set('deploy function', { }, }, lifecycleEvents: ['initialize', 'packageFunction', 'deploy'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('deploy list', { usage: 'List deployed version of your Serverless Service', + options: {}, lifecycleEvents: ['log'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); + commands.set('deploy list functions', { usage: 'List all the deployed functions and their versions', + options: {}, lifecycleEvents: ['log'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('info', { @@ -81,6 +271,8 @@ commands.set('info', { }, }, lifecycleEvents: ['info'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('invoke', { @@ -88,19 +280,23 @@ commands.set('invoke', { usage: 'Invoke a deployed function', options: { function: { + type: 'string', usage: 'The function name', required: true, shortcut: 'f', }, qualifier: { + type: 'string', usage: 'Version number or alias to invoke', shortcut: 'q', }, path: { + type: 'string', usage: 'Path to JSON or YAML file holding input data', shortcut: 'p', }, type: { + type: 'string', usage: 'Type of invocation', shortcut: 't', }, @@ -110,6 +306,7 @@ commands.set('invoke', { type: 'boolean', }, data: { + type: 'string', usage: 'Input data', shortcut: 'd', }, @@ -118,13 +315,17 @@ commands.set('invoke', { type: 'boolean', }, context: { + type: 'string', usage: 'Context of the service', }, contextPath: { + type: 'string', usage: 'Path to JSON or YAML file holding context data', }, }, lifecycleEvents: ['invoke'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('invoke local', { @@ -132,15 +333,18 @@ commands.set('invoke local', { usage: 'Invoke function locally', options: { 'function': { + type: 'string', usage: 'Name of the function', shortcut: 'f', required: true, }, 'path': { + type: 'string', usage: 'Path to JSON or YAML file holding input data', shortcut: 'p', }, 'data': { + type: 'string', usage: 'input data', shortcut: 'd', }, @@ -149,9 +353,11 @@ commands.set('invoke local', { type: 'boolean', }, 'context': { + type: 'string', usage: 'Context of the service', }, 'contextPath': { + type: 'string', usage: 'Path to JSON or YAML file holding context data', shortcut: 'x', }, @@ -160,12 +366,18 @@ commands.set('invoke local', { shortcut: 'e', type: 'multiple', }, - 'docker': { usage: 'Flag to turn on docker use for node/python/ruby/java', type: 'boolean' }, + 'docker': { + usage: 'Flag to turn on docker use for node/python/ruby/java', + type: 'boolean' + }, 'docker-arg': { + type: 'string', usage: 'Arguments to docker run command. e.g. --docker-arg "-p 9229:9229"', }, }, lifecycleEvents: ['loadEnvVars', 'invoke'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('logs', { @@ -173,6 +385,7 @@ commands.set('logs', { usage: 'Output the logs of a deployed function', options: { function: { + type: 'string', usage: 'The function name', required: true, shortcut: 'f', @@ -183,106 +396,107 @@ commands.set('logs', { type: 'boolean', }, startTime: { + type: 'string', usage: 'Logs before this time will not be displayed. Default: `10m` (last 10 minutes logs only)', }, filter: { + type: 'string', usage: 'A filter pattern', }, interval: { + type: 'string', usage: 'Tail polling interval in milliseconds. Default: `1000`', shortcut: 'i', }, }, lifecycleEvents: ['logs'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('metrics', { usage: 'Show metrics for a specific function', options: { function: { + type: 'string', usage: 'The function name', shortcut: 'f', }, startTime: { + type: 'string', usage: 'Start time for the metrics retrieval (e.g. 1970-01-01)', }, endTime: { + type: 'string', usage: 'End time for the metrics retrieval (e.g. 1970-01-01)', }, }, lifecycleEvents: ['metrics'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('remove', { usage: 'Remove Serverless service and all resources', + options: {}, lifecycleEvents: ['remove'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('rollback', { usage: 'Rollback the Serverless service to a specific deployment', options: { timestamp: { + type: 'string', usage: 'Timestamp of the deployment (list deployments with `serverless deploy list`)', shortcut: 't', required: false, }, }, lifecycleEvents: ['initialize', 'rollback'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('rollback function', { usage: 'Rollback the function to the previous version', options: { 'function': { + type: 'string', usage: 'Name of the function', shortcut: 'f', required: true, }, 'function-version': { + type: 'string', usage: 'Version of the function', required: true, }, }, lifecycleEvents: ['rollback'], + serviceDependencyMode: 'required', + hasAwsExtension: true }); commands.set('test', { usage: 'Run HTTP tests', options: { function: { + type: 'string', usage: 'Specify the function to test', shortcut: 'f', }, test: { + type: 'string', usage: 'Specify a specific test to run', shortcut: 't', }, }, lifecycleEvents: ['test'], + hasAwsExtension: true, + serviceDependencyMode: 'required', }); -for (const schema of commands.values()) { - schema.serviceDependencyMode = 'required'; - schema.hasAwsExtension = true; - if (!schema.options) schema.options = {}; - for (const optionSchema of Object.values(schema.options)) { - if (!optionSchema.type) optionSchema.type = 'string'; - } - Object.assign(schema.options, awsServiceOptions); -} - -for (const [name, schema] of serviceCommands) commands.set(name, schema); - -const packageCommandConfig = commands.get('package'); -commands.set('package', { - ...packageCommandConfig, - options: { - ...packageCommandConfig.options, - 'minify-template': { - usage: 'Minify the CloudFormation template for AWS packages', - type: 'boolean', - }, - }, -}); diff --git a/lib/cli/commands-schema/common-options/aws-service.js b/lib/cli/commands-schema/common-options/aws-service.js deleted file mode 100644 index 85f78bb97..000000000 --- a/lib/cli/commands-schema/common-options/aws-service.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -module.exports = { - 'region': { - usage: 'Region of the service', - shortcut: 'r', - }, - 'aws-profile': { - usage: 'AWS profile to use with the command', - }, - 'app': { usage: 'Serverless Framework Dashboard app' }, - 'org': { usage: 'Serverless Framework Dashboard org' }, - 'use-local-credentials': { - usage: - 'Rely on locally resolved AWS credentials instead of loading them from ' + - 'Serverless Framework Providers. This applies only to services signed into the Serverless Framework Dashboard.', - type: 'boolean', - }, - ...require('./service'), -}; - -for (const optionSchema of Object.values(module.exports)) { - if (!optionSchema.type) optionSchema.type = 'string'; -} diff --git a/lib/cli/commands-schema/common-options/global.js b/lib/cli/commands-schema/common-options/global.js deleted file mode 100644 index f53106bda..000000000 --- a/lib/cli/commands-schema/common-options/global.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -module.exports = { - help: { usage: 'Show this message', shortcut: 'h', type: 'boolean' }, - version: { usage: 'Show version info', shortcut: 'v', type: 'boolean' }, - verbose: { usage: 'Show verbose logs', type: 'boolean' }, - debug: { usage: 'Namespace of debug logs to expose (use "*" to display all)', type: 'string' }, -}; - -for (const optionSchema of Object.values(module.exports)) { - if (!optionSchema.type) optionSchema.type = 'string'; -} diff --git a/lib/cli/commands-schema/common-options/service.js b/lib/cli/commands-schema/common-options/service.js deleted file mode 100644 index f7c9da3d6..000000000 --- a/lib/cli/commands-schema/common-options/service.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -module.exports = { - config: { - usage: 'Path to serverless config file', - shortcut: 'c', - }, - stage: { - usage: 'Stage of the service', - shortcut: 's', - }, - param: { - usage: 'Pass custom parameter values for "param" variable source (usage: --param="key=value")', - type: 'multiple', - }, - ...require('./global'), -}; - -for (const optionSchema of Object.values(module.exports)) { - if (!optionSchema.type) optionSchema.type = 'string'; -} diff --git a/lib/cli/commands-schema/index.js b/lib/cli/commands-schema/index.js deleted file mode 100644 index ad004d85e..000000000 --- a/lib/cli/commands-schema/index.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports = require('./aws-service'); diff --git a/lib/cli/commands-schema/no-service.js b/lib/cli/commands-schema/no-service.js deleted file mode 100644 index 9d4514fa9..000000000 --- a/lib/cli/commands-schema/no-service.js +++ /dev/null @@ -1,228 +0,0 @@ -'use strict'; - -const globalOptions = require('./common-options/global'); -const serviceOptions = require('./common-options/service'); -const awsServiceOptions = require('./common-options/aws-service'); - -const commands = (module.exports = new Map()); - -commands.commonOptions = globalOptions; - -commands.set('', { - usage: 'Interactive Quickstart', - serviceDependencyMode: 'optional', - hasAwsExtension: true, - options: { - 'help-interactive': { usage: 'Show this message', type: 'boolean' }, - 'name': { - usage: 'Name for the service.', - }, - 'template': { - usage: 'Name of template for the service.', - }, - 'template-path': { - usage: 'Template local path for the service.', - }, - 'template-url': { - usage: 'Template url for the service.', - }, - 'function': { - usage: 'Name of the function you would like the dev mode activity feed to observe.', - type: 'string', - shortcut: 'f', - }, - }, - lifecycleEvents: ['initializeService', 'setupAws', 'autoUpdate', 'end'], -}); - -commands.set('config', { - usage: 'Configure Serverless', - options: { - autoupdate: { - usage: 'Turn on auto update mechanism (turn off via "--no-autoupdate")', - type: 'boolean', - }, - }, - lifecycleEvents: ['config'], -}); - -commands.set('config credentials', { - usage: 'Configures a new provider profile for the Serverless Framework', - hasAwsExtension: true, - options: { - provider: { - usage: 'Name of the provider. Supported providers: aws', - required: true, - shortcut: 'p', - }, - key: { - usage: 'Access key for the provider', - shortcut: 'k', - required: true, - }, - secret: { - usage: 'Secret key for the provider', - shortcut: 's', - required: true, - }, - profile: { - usage: 'Name of the profile you wish to create. Defaults to "default"', - shortcut: 'n', - }, - overwrite: { - usage: 'Overwrite the existing profile configuration in the credentials file', - shortcut: 'o', - type: 'boolean', - }, - }, - lifecycleEvents: ['config'], -}); - -commands.set('create', { - usage: 'Create new Serverless service', - options: { - 'template': { - usage: - 'Template for the service. Available templates: ' + - `${require('../../templates/recommended-list/human-readable')}`, - shortcut: 't', - }, - 'template-url': { - usage: 'Template URL for the service. Supports: GitHub, BitBucket', - shortcut: 'u', - }, - 'template-path': { - usage: 'Template local path for the service.', - }, - 'path': { - usage: 'The path where the service should be created (e.g. --path my-service)', - shortcut: 'p', - }, - 'name': { - usage: 'Name for the service. Overwrites the default name of the created service.', - shortcut: 'n', - }, - }, - lifecycleEvents: ['create'], -}); - -commands.set('doctor', { - usage: 'Print status on reported deprecations triggered in the last command run', -}); - -commands.set('generate-event', { - usage: 'Generate event', - lifecycleEvents: ['generate-event'], - options: { - type: { - usage: - 'Specify event type. "aws:apiGateway", "aws:sns", "aws:sqs", "aws:dynamo", ' + - '"aws:kinesis", "aws:cloudWatchLog", "aws:s3", "aws:alexaSmartHome", "aws:alexaSkill", ' + - '"aws:cloudWatch", "aws:iot", "aws:cognitoUserPool","aws:websocket" are supported.', - shortcut: 't', - required: true, - }, - body: { - usage: 'Specify the body for the message, request, or stream event.', - shortcut: 'b', - }, - }, -}); - -commands.set('help', { - usage: 'Show this help', - serviceDependencyMode: 'optional', -}); - -commands.set('install', { - usage: 'Install a Serverless service from GitHub or a plugin from the Serverless registry', - options: { - url: { - usage: 'URL of the Serverless service on GitHub', - required: true, - shortcut: 'u', - }, - name: { - usage: 'Name for the service', - shortcut: 'n', - }, - }, - lifecycleEvents: ['install'], -}); - -commands.set('plugin list', { - usage: 'Lists all available plugins', - lifecycleEvents: ['list'], -}); - -commands.set('plugin search', { - usage: 'Search for plugins', - options: { - query: { - usage: 'Search query', - required: true, - shortcut: 'q', - }, - }, - lifecycleEvents: ['search'], -}); - -commands.set('slstats', { - usage: 'Enable or disable stats', - options: { - enable: { - usage: 'Enable stats ("--enable")', - shortcut: 'e', - type: 'boolean', - }, - disable: { - usage: 'Disable stats ("--disable")', - shortcut: 'd', - type: 'boolean', - }, - }, - lifecycleEvents: ['slstats'], -}); - -(() => { - const isHidden = !require('../../utils/is-standalone-executable') || process.platform === 'win32'; - const noSupportNotice = - "It's applicable only in context of a standalone executable instance " + - 'in non Windows environment.'; - - commands.set('upgrade', { - usage: 'Upgrade Serverless', - isHidden, - noSupportNotice, - options: { - major: { - usage: 'Enable upgrade to a new major release', - type: 'boolean', - }, - }, - lifecycleEvents: ['upgrade'], - }); - commands.set('uninstall', { - usage: 'Uninstall Serverless', - isHidden, - noSupportNotice, - lifecycleEvents: ['uninstall'], - }); -})(); - -for (const [name, schema] of commands) { - if (!schema.options) schema.options = {}; - for (const optionSchema of Object.values(schema.options)) { - if (!optionSchema.type) optionSchema.type = 'string'; - } - if (schema.serviceDependencyMode) { - Object.assign(schema.options, schema.hasAwsExtension ? awsServiceOptions : serviceOptions); - } else { - Object.assign(schema.options, globalOptions); - } - if (!name) { - // Necessary tweaks for Interactive CLI help - schema.options.help = { ...schema.options.help, usage: 'Show general help info' }; - schema.options.version = { ...schema.options.version, shortcut: 'v' }; - } -} diff --git a/lib/cli/commands-schema/resolve-final.js b/lib/cli/commands-schema/resolve-final.js deleted file mode 100644 index 2703d07ea..000000000 --- a/lib/cli/commands-schema/resolve-final.js +++ /dev/null @@ -1,124 +0,0 @@ -// Resolves final schema of commands for given service configuration - -'use strict'; - -const _ = require('lodash'); - -const serviceCommands = require('./service'); -const awsServiceCommands = require('./aws-service'); -const serviceOptions = require('./common-options/service'); -const awsServiceOptions = require('./common-options/aws-service'); -const logDeprecation = require('../../utils/log-deprecation'); - -module.exports = (loadedPlugins, { providerName, configuration }) => { - const commands = new Map(providerName === 'aws' ? awsServiceCommands : serviceCommands); - - if (providerName !== 'aws') { - // Recognize AWS provider commands adapted in context of other provider - // Those commands do not have to be defined as "commands" in plugin. - // It's good enough if hooks for command lifecycle events are setup - // and our detection confirms on that. - const optionalServiceCommandsHooksMap = new Map( - Array.from(awsServiceCommands) - .filter(([name]) => !serviceCommands.has(name)) - .map(([name, schema]) => { - const lifecycleEventNamePrefix = name.split(' ').join(':'); - return (schema.lifecycleEvents || []).map((lifecycleEventBaseName) => { - const lifecycleEventName = `${lifecycleEventNamePrefix}:${lifecycleEventBaseName}`; - return [ - [`before:${lifecycleEventName}`, name], - [lifecycleEventName, name], - [`after:${lifecycleEventName}`, name], - ]; - }); - }) - .flat(2) - ); - - const awsSpecificOptionNames = new Set( - Object.keys(awsServiceOptions).filter((optionName) => !serviceOptions[optionName]) - ); - - for (const loadedPlugin of loadedPlugins) { - if (!loadedPlugin.hooks) continue; - for (const hookName of Object.keys(loadedPlugin.hooks)) { - const awsCommandName = optionalServiceCommandsHooksMap.get(hookName); - if (awsCommandName && !commands.has(awsCommandName)) { - const schema = _.merge({}, awsServiceCommands.get(awsCommandName), { - isExtension: true, - sourcePlugin: loadedPlugin, - }); - for (const awsSpecificOptionName of awsSpecificOptionNames) { - delete schema.options[awsSpecificOptionName]; - } - commands.set(awsCommandName, schema); - } - } - } - } - - const missingOptionTypes = new Map(); - const commonOptions = providerName === 'aws' ? awsServiceOptions : serviceOptions; - commands.commonOptions = commonOptions; - const resolveCommands = (loadedPlugin, config, commandPrefix = '') => { - if (!config.commands) return; - for (const [commandName, commandConfig] of Object.entries(config.commands)) { - if (commandConfig.type === 'entrypoint') continue; - const fullCommandName = `${commandPrefix}${commandName}`; - if (commandConfig.type !== 'container') { - const schema = commands.has(fullCommandName) - ? _.merge({}, commands.get(fullCommandName)) - : { - usage: commandConfig.usage, - serviceDependencyMode: 'required', - isExtension: true, - sourcePlugin: loadedPlugin, - isHidden: commandConfig.isHidden, - noSupportNotice: commandConfig.noSupportNotice, - options: {}, - }; - if (commandConfig.lifecycleEvents) schema.lifecycleEvents = commandConfig.lifecycleEvents; - if (commandConfig.options) { - for (const [optionName, optionConfig] of Object.entries(commandConfig.options)) { - if (!schema.options[optionName]) { - schema.options[optionName] = optionConfig; - if (!optionConfig.type) { - if (!missingOptionTypes.has(loadedPlugin)) { - missingOptionTypes.set(loadedPlugin, new Set()); - } - missingOptionTypes.get(loadedPlugin).add(optionName); - } - } - } - } - - // Put common options to end of index - for (const optionName of Object.keys(commonOptions)) delete schema.options[optionName]; - Object.assign(schema.options, commonOptions); - - commands.set(fullCommandName, schema); - } - resolveCommands(loadedPlugin, commandConfig, `${fullCommandName} `); - } - }; - - for (const loadedPlugin of loadedPlugins) resolveCommands(loadedPlugin, loadedPlugin); - - if (missingOptionTypes.size) { - logDeprecation( - 'CLI_OPTIONS_SCHEMA_V3', - 'CLI options definitions were upgraded with "type" property (which could be one of "string", "boolean", "multiple"). ' + - 'Below listed plugins do not predefine type for introduced options:\n' + - ` - ${Array.from( - missingOptionTypes, - ([plugin, optionNames]) => - `${plugin.constructor.name} for "${Array.from(optionNames).join('", "')}"` - ).join('\n - ')}\n` + - 'Please report this issue in plugin issue tracker.\n' + - 'Starting with next major release, this will be communicated with a thrown error.', - { serviceConfig: configuration } - ); - } - - return commands; -}; diff --git a/lib/cli/commands-schema/service.js b/lib/cli/commands-schema/service.js deleted file mode 100644 index 17839e392..000000000 --- a/lib/cli/commands-schema/service.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict'; - -const serviceOptions = require('./common-options/service'); -const awsServiceOptions = require('./common-options/aws-service'); -const noServiceCommands = require('./no-service'); - -const commands = (module.exports = new Map()); - -commands.commonOptions = serviceOptions; - -commands.set('package', { - usage: 'Packages a Serverless service', - hasAwsExtension: true, - options: { - package: { - usage: 'Output path for the package', - shortcut: 'p', - }, - }, - lifecycleEvents: [ - 'cleanup', - 'initialize', - 'setupProviderConfiguration', - 'createDeploymentArtifacts', - 'compileLayers', - 'compileFunctions', - 'compileEvents', - 'finalize', - ], -}); - -commands.set('plugin install', { - usage: 'Install and add a plugin to your service', - options: { - name: { - usage: 'The plugin name', - required: true, - shortcut: 'n', - }, - }, - lifecycleEvents: ['install'], -}); - -commands.set('plugin uninstall', { - usage: 'Uninstall and remove a plugin from your service', - options: { - name: { - usage: 'The plugin name', - required: true, - shortcut: 'n', - }, - }, - lifecycleEvents: ['uninstall'], -}); - -commands.set('print', { - usage: 'Print your compiled and resolved config file', - hasAwsExtension: true, - options: { - format: { - usage: 'Print configuration in given format ("yaml", "json", "text"). Default: yaml', - }, - path: { - usage: 'Optional period-separated path to print a sub-value (eg: "provider.name")', - }, - transform: { - usage: 'Optional transform-function to apply to the value ("keys")', - }, - }, - lifecycleEvents: ['print'], -}); - -for (const schema of commands.values()) { - schema.serviceDependencyMode = 'required'; - if (!schema.options) schema.options = {}; - for (const optionSchema of Object.values(schema.options)) { - if (!optionSchema.type) optionSchema.type = 'string'; - } - Object.assign(schema.options, schema.hasAwsExtension ? awsServiceOptions : serviceOptions); -} - -for (const [name, schema] of noServiceCommands) commands.set(name, schema); diff --git a/lib/cli/filter-supported-options.js b/lib/cli/filter-supported-options.js index a59c56e4f..77febc7bf 100644 --- a/lib/cli/filter-supported-options.js +++ b/lib/cli/filter-supported-options.js @@ -1,12 +1,11 @@ 'use strict'; -const serviceOptionsNames = Object.keys(require('./commands-schema/common-options/service')); -const awsServiceOptionsNames = Object.keys(require('./commands-schema/common-options/aws-service')); +const globalOptions = require('./commands-options-schema'); -module.exports = (options, { commandSchema, providerName }) => { +module.exports = (options, { commandSchema }) => { const supportedNames = (() => { if (commandSchema) return Object.keys(commandSchema.options); - return providerName === 'aws' ? awsServiceOptionsNames : serviceOptionsNames; + return globalOptions; })(); const result = Object.create(null); for (const name of supportedNames) result[name] = options[name] == null ? null : options[name]; diff --git a/lib/cli/handle-error.js b/lib/cli/handle-error.js index ecd5cb6aa..8b767b1b3 100644 --- a/lib/cli/handle-error.js +++ b/lib/cli/handle-error.js @@ -7,9 +7,6 @@ const slsVersion = require('./../../package').version; const isStandaloneExecutable = require('../utils/is-standalone-executable'); const tokenizeException = require('../utils/tokenize-exception'); const isLocallyInstalled = require('./is-locally-installed'); -const resolveErrorLocation = require('../utils/telemetry/resolve-error-location'); - -const isErrorCodeNormative = RegExp.prototype.test.bind(/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/); module.exports = async (exception, options = {}) => { if (!isObject(options)) options = {}; @@ -70,10 +67,5 @@ module.exports = async (exception, options = {}) => { process.exitCode = 1; - const failureReason = { kind: isUserError ? 'user' : 'programmer', code: exception.code }; - if (!isUserError || !exception.code || !isErrorCodeNormative(exception.code)) { - failureReason.location = resolveErrorLocation(exceptionTokens); - } - - return { telemetryData: { failureReason, outcome: 'failure' } }; + return { }; }; diff --git a/lib/cli/parse-args.js b/lib/cli/parse-args.js index a5020aaf8..d52d2597b 100644 --- a/lib/cli/parse-args.js +++ b/lib/cli/parse-args.js @@ -1,4 +1,21 @@ -// TODO: Consider publishing as independent package +/** + * Exports a function that parses command + * line arguments provided as an array (args). + * It categorizes these arguments based on their + * types (boolean, string, multiple, alias) and + * stores them in a result object. It also handles + * special cases such as help requests (-h or --help) + * and unexpected values for aliases. + * + * Example return value: + * { + * _: ['otherArgs'], + * booleanArg: true, + * stringArg: 'stringValue', + * multipleArg: ['value1', 'value2'], + * aliasArg: 'aliasValue' + * } + */ 'use strict'; diff --git a/lib/cli/render-help/general.js b/lib/cli/render-help/general.js index df002ed95..abe2ca1c7 100644 --- a/lib/cli/render-help/general.js +++ b/lib/cli/render-help/general.js @@ -2,7 +2,7 @@ const { writeText, style } = require('@serverless/utils/log'); const { version } = require('../../../package'); -const globalOptions = require('../commands-schema/common-options/global'); +const globalOptions = require('../commands-options-schema'); const resolveInput = require('../resolve-input'); const generateCommandUsage = require('./generate-command-usage'); const renderOptions = require('./options'); diff --git a/lib/cli/render-help/index.js b/lib/cli/render-help/index.js index c1da2506e..7ae5ecb74 100644 --- a/lib/cli/render-help/index.js +++ b/lib/cli/render-help/index.js @@ -1,19 +1,12 @@ 'use strict'; const resolveInput = require('../resolve-input'); -const renderInteractiveSetupHelp = require('./interactive-setup'); const renderGeneralHelp = require('./general'); const renderCommandHelp = require('./command'); module.exports = (loadedPlugins) => { - const { command, options } = resolveInput(); - if (!command) { - if (options['help-interactive']) { - renderInteractiveSetupHelp(); - return; - } - renderGeneralHelp(loadedPlugins); - } else if (command === 'help') { + const { command } = resolveInput(); + if (command === 'help') { renderGeneralHelp(loadedPlugins); } else { renderCommandHelp(command); diff --git a/lib/cli/render-help/interactive-setup.js b/lib/cli/render-help/interactive-setup.js index 6e8d16d98..1a1fb6e8c 100644 --- a/lib/cli/render-help/interactive-setup.js +++ b/lib/cli/render-help/interactive-setup.js @@ -1,7 +1,7 @@ 'use strict'; const { writeText, style } = require('@serverless/utils/log'); -const commmandSchema = require('../commands-schema/no-service').get(''); +const commandSchema = require('../commands-schema').get(''); const renderOptions = require('./options'); module.exports = () => { @@ -12,5 +12,5 @@ module.exports = () => { style.aside('Options') ); - renderOptions(commmandSchema.options); + renderOptions(commandSchema.options); }; diff --git a/lib/cli/resolve-input-final.js b/lib/cli/resolve-input-final.js new file mode 100644 index 000000000..15dc71458 --- /dev/null +++ b/lib/cli/resolve-input-final.js @@ -0,0 +1,97 @@ +'use strict'; + +const _ = require('lodash'); +const commandSchema = require('./commands-schema'); +const serviceOptions = require('./commands-options-schema'); +const logDeprecation = require('../utils/log-deprecation'); + +/** + * This module exports a function that processes loaded plugins and their + * configurations, mapping out their commands and options into a commands Map. + * It imports AWS service commands and common options, and merges them with + * the commands and options from the loaded plugins. It also handles missing + * option types by logging a deprecation warning. + * + * @param {Array} loadedPlugins - An array of loaded plugin objects. + * @param {Object} configuration - The configuration object for the plugins. + * @returns {Map} A Map object of commands and their options. + */ +module.exports = (loadedPlugins, { configuration }) => { + const commands = new Map(commandSchema); + const missingOptionTypes = new Map(); + const commonOptions = serviceOptions; + commands.commonOptions = commonOptions; + + /** + * Recursively processes a configuration object for a plugin, mapping out its + * commands and their options. It skips commands of type 'entrypoint', and for + * other commands, it creates or updates a schema with details like usage, + * lifecycle events, and options. If an option is missing the 'type' property, + * it is added to the `missingOptionTypes` set for later processing. + * + * @param {Object} loadedPlugin - The loaded plugin object. + * @param {Object} config - The configuration object for the plugin. + * @param {string} commandPrefix - The prefix for the command (default is ''). + */ + const resolveCommands = (loadedPlugin, config, commandPrefix = '') => { + if (!config.commands) return; + for (const [commandName, commandConfig] of Object.entries(config.commands)) { + if (commandConfig.type === 'entrypoint') continue; + const fullCommandName = `${commandPrefix}${commandName}`; + if (commandConfig.type !== 'container') { + const schema = commands.has(fullCommandName) + ? _.merge({}, commands.get(fullCommandName)) + : { + usage: commandConfig.usage, + serviceDependencyMode: 'required', + isExtension: true, + sourcePlugin: loadedPlugin, + isHidden: commandConfig.isHidden, + noSupportNotice: commandConfig.noSupportNotice, + options: {}, + }; + if (commandConfig.lifecycleEvents) schema.lifecycleEvents = commandConfig.lifecycleEvents; + if (commandConfig.options) { + for (const [optionName, optionConfig] of Object.entries(commandConfig.options)) { + if (!schema.options[optionName]) { + schema.options[optionName] = optionConfig; + if (!optionConfig.type) { + if (!missingOptionTypes.has(loadedPlugin)) { + missingOptionTypes.set(loadedPlugin, new Set()); + } + missingOptionTypes.get(loadedPlugin).add(optionName); + } + } + } + } + + // Put common options to end of index + for (const optionName of Object.keys(commonOptions)) delete schema.options[optionName]; + Object.assign(schema.options, commonOptions); + + commands.set(fullCommandName, schema); + } + resolveCommands(loadedPlugin, commandConfig, `${fullCommandName} `); + } + }; + + for (const loadedPlugin of loadedPlugins) resolveCommands(loadedPlugin, loadedPlugin); + + if (missingOptionTypes.size) { + logDeprecation( + 'CLI_OPTIONS_SCHEMA_V3', + 'CLI options definitions were upgraded with "type" property (which could be one of "string", "boolean", "multiple"). ' + + 'Below listed plugins do not predefine type for introduced options:\n' + + ` - ${Array.from( + missingOptionTypes, + ([plugin, optionNames]) => + `${plugin.constructor.name} for "${Array.from(optionNames).join('", "')}"` + ).join('\n - ')}\n` + + 'Please report this issue in plugin issue tracker.\n' + + 'Starting with next major release, this will be communicated with a thrown error.', + { serviceConfig: configuration } + ); + } + + return commands; +}; diff --git a/lib/cli/resolve-input.js b/lib/cli/resolve-input.js index dc6d82d29..bb5eebd22 100644 --- a/lib/cli/resolve-input.js +++ b/lib/cli/resolve-input.js @@ -1,4 +1,7 @@ -// CLI params parser, to be used before we have deducted what commands and options are supported in given context +/** + * CLI params parser, to be used before we have deducted + * what commands and options are supported in given context. + */ 'use strict'; @@ -8,6 +11,15 @@ const parseArgs = require('./parse-args'); const isParamName = RegExp.prototype.test.bind(require('./param-reg-exp')); +/** + * This JavaScript function takes a schema of command + * line options and organizes them into categories + * (boolean, string, multiple, alias) based on their + * types and shortcuts, returning an object with these + * categorized options. + * @param {*} commandOptionsSchema + * @returns + */ const resolveArgsSchema = (commandOptionsSchema) => { const options = { boolean: new Set(), string: new Set(), alias: new Map(), multiple: new Set() }; for (const [name, optionSchema] of Object.entries(commandOptionsSchema)) { @@ -28,6 +40,13 @@ const resolveArgsSchema = (commandOptionsSchema) => { return options; }; +/** + * Parses command line arguments, identifies the command and + * its options, and returns an object containing these + * details. It also determines if the command is a + * container command or if a help request has been made, + * and sets corresponding flags in the returned object. + */ module.exports = memoizee((commandsSchema = require('./commands-schema')) => { commandsSchema = ensureMap(commandsSchema); const args = process.argv.slice(2); diff --git a/lib/configuration/resolve-variables.js b/lib/configuration/resolve-variables.js new file mode 100644 index 000000000..f2af15851 --- /dev/null +++ b/lib/configuration/resolve-variables.js @@ -0,0 +1,471 @@ +'use strict'; + +// Import required modules +const path = require('path'); +const _ = require('lodash'); +const ServerlessError = require('../serverless-error'); +const resolveVariables = require('./variables/resolve'); +const resolveVariablesMeta = require('./variables/resolve-meta'); +const isPropertyResolved = require('./variables/is-property-resolved'); +const eventuallyReportVariableResolutionErrors = require('./variables/eventually-report-resolution-errors'); +const filterSupportedOptions = require('../cli/filter-supported-options'); +const humanizePropertyPathKeys = require('./variables/humanize-property-path-keys'); +const serverlessVariablesSourceEnv = require('./variables/sources/env'); +const serverlessVariablesSourceFile = require('./variables/sources/file'); +const serverlessVariablesSourceOpt = require('./variables/sources/opt'); +const serverlessVariablesSourceSelf = require('./variables/sources/self'); +const serverlessVariablesSourceStrToBool = require('./variables/sources/str-to-bool'); +const serverlessVariablesSourceSls = require('./variables/sources/sls'); +const serverlessVariablesSourceParams = require('./variables/sources/param'); +const serverlessVariablesSourceAwsCf = require('./variables/sources/aws-cf'); +const serverlessVariablesSourceAwsS3 = require('./variables/sources/aws-s3'); +const serverlessVariablesSourceAwsSsm = require('./variables/sources/aws-ssm'); +const serverlessVariablesSourceAws = require('./variables/sources/aws'); +const resolveExternalPluginSources = require('./variables/sources/resolve-external-plugin-sources'); + +/** + * Resolve Serverless Variables: Phase One + * + * Resolve Serverless Variables "provider.stage" and "useDotenv" + * These need to be resolved before any other variables, since a lot of + * logic is dependent on them. + */ +const resolveServerlessVariablesPhaseOne = async ({ + service, + servicePath, + options, +}) => { + + const sourcesToResolveFrom = [ + 'file', + 'self', + 'strToBool', + 'opt', + 'env', + ]; + const propertyPathsToResolve = [ + 'provider\0stage', + 'useDotenv' + ]; + + // Creates a Map for each variable found in the configuration. + const variablesMeta = resolveVariablesMeta(service) + + // Configure resolver + const resolverConfiguration = { + serviceDir: servicePath, + configuration: service, + variablesMeta, + sources: { + file: serverlessVariablesSourceFile, + self: serverlessVariablesSourceSelf, + strToBool: serverlessVariablesSourceStrToBool, + opt: serverlessVariablesSourceOpt, + env: serverlessVariablesSourceEnv, + }, + options, + fulfilledSources: new Set(sourcesToResolveFrom), + propertyPathsToResolve: new Set(propertyPathsToResolve), + variableSourcesInConfig: new Set(), + }; + + await resolveVariables(resolverConfiguration); + + const resolvedService = resolverConfiguration.configuration; + + // Throw any resolution errors + const resolutionErrors = new Set( + Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean) + ); + if (resolutionErrors.size) { + throw new ServerlessError( + `Cannot resolve ${path.basename( + servicePath + )}: Variables resolution errored with:${Array.from( + resolutionErrors, + (error) => `\n - ${error.message}` + )}`, + 'VARIABLES_RESOLUTION_ERROR' + ); + } + + /** + * Massage useDotenv to be a boolean + */ + if (resolvedService.useDotenv === 'true') { resolvedService.useDotenv = true; } + if (resolvedService.useDotenv === 'false') { resolvedService.useDotenv = false; } + + /** + * If "provider.stage" or "useDotenv" variables were not resolved and + * include "${env:", report them and explain what might have happened. + */ + if (!isPropertyResolved(variablesMeta, 'provider\0stage') && + resolvedService.provider.stage.includes('${env:')) { + throw new ServerlessError( + 'Could not resolve "provider.stage" variable. Environment variable is missing. Please note that if the environment variable is specified in a .env file, it is not loaded until after the "provider.stage" variable is resolved. The reason is .env file loading looks for .env.${stage} as well as .env but when "provider.stage" is not resolved, the stage is unknown.', + 'VARIABLES_RESOLUTION_ERROR' + ); + } + if (!isPropertyResolved(variablesMeta, 'useDotenv') && + resolvedService.useDotenv.includes('${env:')) { + throw new ServerlessError( + 'Could not resolve "useDotenv" variable. Environment variable is missing. Please note that if the environment variable is specified in a .env file, it is not loaded until after the "useDotenv" variable is resolved. The reason is .env file loading looks for .env.${stage} as well as .env but when "useDotenv" is not resolved, the stage is unknown.', + 'VARIABLES_RESOLUTION_ERROR' + ); + } + + /** + * Throw error if key properteis are not resolved with + * helpful comments on the rationale. + */ + if (!isPropertyResolved(variablesMeta, 'provider\0stage')) { + throw new ServerlessError( + `Cannot resolve "provider.stage" variable. Please note, only Variable sources ${sourcesToResolveFrom.join(', ')} are supported for this property. AWS sources (e.g. SSM) or Serverless Framework Dashboard sources (e.g. params) are not supported for this property.`, + 'VARIABLES_RESOLUTION_ERROR' + ); + } + if (!isPropertyResolved(variablesMeta, 'useDotenv')) { + throw new ServerlessError( + `Cannot resolve "useDotenv" variable. Please note, only Variable sources ${sourcesToResolveFrom.join(', ')} are supported for this property. AWS sources (e.g. SSM) or Serverless Framework Dashboard sources (e.g. params) are not supported for this property.`, + 'VARIABLES_RESOLUTION_ERROR' + ); + } + + return resolvedService; +} + +/** + * Resolve Serverless Variables: Phase Two + * + * Resolve everything, but still using limited sources. + * We need to get "params" from the Platform for Dashboard + * users, but need to ensure that some properties are resolved + * (e.g. "org", "app", "service", "region", etc.) + */ +const resolveServerlessVariablesPhaseTwo = async ({ + service, + servicePath, + options, +}) => { + + const sourcesToResolveFrom = [ + 'file', + 'self', + 'strToBool', + 'opt', + 'env', + ]; + + // Creates a Map for each variable found in the configuration. + const variablesMeta = resolveVariablesMeta(service) + + // Configure resolver + const resolverConfiguration = { + serviceDir: servicePath, + configuration: service, + variablesMeta, + sources: { + file: serverlessVariablesSourceFile, + self: serverlessVariablesSourceSelf, + strToBool: serverlessVariablesSourceStrToBool, + opt: serverlessVariablesSourceOpt, + env: serverlessVariablesSourceEnv, + }, + options, + fulfilledSources: new Set(sourcesToResolveFrom), + variableSourcesInConfig: new Set(), + // propertyPathsToResolve: new Set([]), // Omit to resolve everything + }; + + await resolveVariables(resolverConfiguration); + + const resolvedService = resolverConfiguration.configuration; + + // Throw any resolution errors + const resolutionErrors = new Set( + Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean) + ); + if (resolutionErrors.size) { + throw new ServerlessError( + `Cannot resolve ${path.basename( + servicePath + )}: Variables resolution errored with:${Array.from( + resolutionErrors, + (error) => `\n - ${error.message}` + )}`, + 'VARIABLES_RESOLUTION_ERROR' + ); + } + + /** + * Throw errors for common mistakes to help users. + */ + const errorHandler = (property) => { + throw new ServerlessError( + `Cannot resolve "${property}" variable. Please note, only Variable sources ${sourcesToResolveFrom.join(', ')} are supported for this property. AWS sources (e.g. SSM) or Serverless Framework Dashboard sources (e.g. params) are not supported for this property.`, + 'VARIABLES_RESOLUTION_ERROR' + ); + } + if (!isPropertyResolved(variablesMeta, 'package\0path')) { return errorHandler('package\0path'); } + if (!isPropertyResolved(variablesMeta, 'frameworkVersion')) { return errorHandler('frameworkVersion'); } + if (!isPropertyResolved(variablesMeta, 'org')) { return errorHandler('org'); } + if (!isPropertyResolved(variablesMeta, 'app')) { return errorHandler('app'); } + if (!isPropertyResolved(variablesMeta, 'service')) { return errorHandler('service'); } + if (!isPropertyResolved(variablesMeta, 'provider\0region')) { return errorHandler('provider\0region'); } + if (!isPropertyResolved(variablesMeta, 'dashboard')) { return errorHandler('dashboard'); } + + return resolvedService; +} + +/** + * Resolve Serverless Variables: Phase Three + * + * Resolve everything except for Plugin sources + */ +const resolveServerlessVariablesPhaseThree = async ({ + service, + servicePath, + options, + serverlessFrameworkInstance, + serviceInstanceParamsFromPlatform, + serviceOutputReferencesFromPlatform = {}, +}) => { + + let sourcesToResolveFrom = [ + 'file', + 'self', + 'strToBool', + 'opt', + 'env', + 'sls', + 'param', + ]; + + // Creates a Map for each variable found in the configuration. + const variablesMeta = resolveVariablesMeta(service) + + /** + * First resolve, without AWS sources + */ + let resolverConfiguration = { + serviceDir: servicePath, + configuration: service, + variablesMeta, + sources: { + file: serverlessVariablesSourceFile, + self: serverlessVariablesSourceSelf, + strToBool: serverlessVariablesSourceStrToBool, + opt: serverlessVariablesSourceOpt, + env: serverlessVariablesSourceEnv, + sls: serverlessVariablesSourceSls(serverlessFrameworkInstance), + param: serverlessVariablesSourceParams({ + service, + serviceInstanceParamsFromPlatform + }), + }, + options, + fulfilledSources: new Set(sourcesToResolveFrom), + variableSourcesInConfig: new Set(), + // propertyPathsToResolve: new Set([]), // Omit to resolve everything + }; + await resolveVariables(resolverConfiguration); + let resolvedService = resolverConfiguration.configuration; + + // Throw any resolution errors + let resolutionErrors = new Set( + Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean) + ); + if (resolutionErrors.size) { + throw new ServerlessError( + `Cannot resolve ${path.basename( + servicePath + )}: Variables resolution errored with:${Array.from( + resolutionErrors, + (error) => `\n - ${error.message}` + )}`, + 'VARIABLES_RESOLUTION_ERROR' + ); + } + + /** + * Second resolve, with AWS sources + */ + sourcesToResolveFrom = [ + 'file', + 'self', + 'strToBool', + 'opt', + 'env', + 'sls', + 'param', + 'cf', + 's3', + 'ssm', + 'aws', + ]; + + resolverConfiguration = { + serviceDir: servicePath, + configuration: service, + variablesMeta, + sources: { + file: serverlessVariablesSourceFile, + self: serverlessVariablesSourceSelf, + strToBool: serverlessVariablesSourceStrToBool, + opt: serverlessVariablesSourceOpt, + env: serverlessVariablesSourceEnv, + sls: serverlessVariablesSourceSls(serverlessFrameworkInstance), + param: serverlessVariablesSourceParams({ + service, + serviceInstanceParamsFromPlatform + }), + cf: serverlessVariablesSourceAwsCf(serverlessFrameworkInstance), + s3: serverlessVariablesSourceAwsS3(serverlessFrameworkInstance), + ssm: serverlessVariablesSourceAwsSsm(serverlessFrameworkInstance), + aws: serverlessVariablesSourceAws(serverlessFrameworkInstance), + }, + options, + fulfilledSources: new Set(sourcesToResolveFrom), + variableSourcesInConfig: new Set(), + // propertyPathsToResolve: new Set([]), // Omit to resolve everything + }; + await resolveVariables(resolverConfiguration); + resolvedService = resolverConfiguration.configuration; + + // Throw any resolution errors + resolutionErrors = new Set( + Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean) + ); + if (resolutionErrors.size) { + throw new ServerlessError( + `Cannot resolve ${path.basename( + servicePath + )}: Variables resolution errored with:${Array.from( + resolutionErrors, + (error) => `\n - ${error.message}` + )}`, + 'VARIABLES_RESOLUTION_ERROR' + ); + } + + return resolvedService; +} + +/** + * Resolve Serverless Variables: Phase Four + * + * Plugins have been loaded at this point, so we can resolve + * any variables from sources they might have added. + */ +const resolveServerlessVariablesPhaseFour = async ({ + service, + servicePath, + options, + serverlessFrameworkInstance, + serviceInstanceParamsFromPlatform, + serviceOutputReferencesFromPlatform = {}, +}) => { + + // Creates a Map for each variable found in the configuration. + const variablesMeta = resolveVariablesMeta(service) + + // Create a full list of sources to resolve from + const sourcesToResolveFrom = [ + 'file', + 'self', + 'strToBool', + 'opt', + 'env', + 'sls', + 'param', + 'cf', + 's3', + 'ssm', + 'aws', + ]; + + const resolverConfiguration = { + serviceDir: servicePath, + configuration: service, + variablesMeta, + sources: { + file: serverlessVariablesSourceFile, + self: serverlessVariablesSourceSelf, + strToBool: serverlessVariablesSourceStrToBool, + opt: serverlessVariablesSourceOpt, + env: serverlessVariablesSourceEnv, + sls: serverlessVariablesSourceSls(serverlessFrameworkInstance), + param: serverlessVariablesSourceParams({ + service, + serviceInstanceParamsFromPlatform + }), + cf: serverlessVariablesSourceAwsCf(serverlessFrameworkInstance), + s3: serverlessVariablesSourceAwsS3(serverlessFrameworkInstance), + ssm: serverlessVariablesSourceAwsSsm(serverlessFrameworkInstance), + aws: serverlessVariablesSourceAws(serverlessFrameworkInstance), + }, + options, + fulfilledSources: new Set(sourcesToResolveFrom), + variableSourcesInConfig: new Set(), + // propertyPathsToResolve: new Set([]), // Omit to resolve everything + }; + + resolveExternalPluginSources( + service, + servicePath, + serverlessFrameworkInstance.pluginManager.externalPlugins + ); + + await resolveVariables(resolverConfiguration); + const resolvedService = resolverConfiguration.configuration; + + // Throw any resolution errors + const resolutionErrors = new Set( + Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean) + ); + if (resolutionErrors.size) { + throw new ServerlessError( + `Cannot resolve ${path.basename( + servicePath + )}: Variables resolution errored with:${Array.from( + resolutionErrors, + (error) => `\n - ${error.message}` + )}`, + 'VARIABLES_RESOLUTION_ERROR' + ); + } + + // Return if no remaining variables exist + if (!variablesMeta.size) { return resolvedService; } + + /** + * Report unrecognized Serverless Variable Sources found + * within the Service configuration, which naturally, are + * still unresolved. + */ + const unresolvedSources = require('./variables/resolve-unresolved-source-types')(variablesMeta) + const recognizedSourceNames = new Set(Object.keys(resolverConfiguration.sources)) + const unrecognizedSourceNames = Array.from(unresolvedSources.keys()).filter( + (sourceName) => !recognizedSourceNames.has(sourceName) + ) + + if (unrecognizedSourceNames.includes('output')) { + throw new ServerlessError( + '"Cannot resolve configuration: ' + + '"output" variable can only be used in ' + + 'services deployed with Serverless Dashboard (with "org" setting configured)', + 'DASHBOARD_VARIABLE_SOURCES_MISUSE' + ) + } + throw new ServerlessError( + `Unrecognized configuration variable sources: "${unrecognizedSourceNames.join('", "')}"`, + 'UNRECOGNIZED_VARIABLE_SOURCES' + ) +} + +module.exports = { + resolveServerlessVariablesPhaseOne, + resolveServerlessVariablesPhaseTwo, + resolveServerlessVariablesPhaseThree, + resolveServerlessVariablesPhaseFour, +}; diff --git a/lib/configuration/variables/humanize-property-path-keys.js b/lib/configuration/variables/humanize-property-path-keys.js index 45db9c6b1..f2b62d69b 100644 --- a/lib/configuration/variables/humanize-property-path-keys.js +++ b/lib/configuration/variables/humanize-property-path-keys.js @@ -1,4 +1,7 @@ -// Stringify property keys array for user facing message +/** + * Stringify property keys array for user facing message + * For example, if the input is ['a', 'b', 'c'], the output will be 'a.b.c' + */ 'use strict'; diff --git a/lib/configuration/variables/index.js b/lib/configuration/variables/index.js index 488f23467..992d373d6 100644 --- a/lib/configuration/variables/index.js +++ b/lib/configuration/variables/index.js @@ -1,5 +1,21 @@ -// Resolves all non instance dependent variables in a provided configuration -// This util is not used in Serverless process flow, but is handy for side resolution of variables +/* + * NOTE: NOT used in the main Serverless process flow. + * + * This utility module resolves all non-instance dependent variables in a + * provided configuration. Although it's not used in the main Serverless + * process flow, it's useful for side resolution of variables. + * + * The module imports necessary dependencies and defines default sources for + * variable resolution, including environment variables, file sources, options, + * self references, string-to-boolean conversions, and Serverless instance data. + * + * It also includes a function to report any errors that occur during variable + * resolution, throwing a ServerlessError with a detailed message if any errors + * are found. + * + * The module exports an asynchronous function that takes a configuration object + * as input, which includes the service directory, service path, and configuration. + */ 'use strict'; @@ -34,7 +50,13 @@ const reportEventualErrors = (variablesMeta) => { ); }; -module.exports = async ({ serviceDir, servicePath, configuration, options, sources = null }) => { +module.exports = async ({ + serviceDir, + servicePath, + configuration, + options, + sources = null +}) => { serviceDir = ensureString(serviceDir || servicePath); ensurePlainObject(configuration); options = ensurePlainObject(options, { default: {} }); diff --git a/lib/configuration/variables/parse.js b/lib/configuration/variables/parse.js index c48730452..1310f3f06 100644 --- a/lib/configuration/variables/parse.js +++ b/lib/configuration/variables/parse.js @@ -1,4 +1,15 @@ -// Variables syntax parser, for a string input returns AST-like meta object +/* + * This module, parse.js, is a variables syntax parser that returns an AST-like + * meta object for a given string input. + * + * The parser uses "states" to keep track of where it is in the string. Each state + * has a function that handles what to do when the parser is in that state. + * + * There are also functions to finish parsing a source or a variable. These are + * called when the parser reaches the end of a source or variable in the string. + * They add the parsed source or variable to a list and update the state for the + * next source or variable. + */ 'use strict'; diff --git a/lib/configuration/variables/resolve-meta.js b/lib/configuration/variables/resolve-meta.js index 810d8c79e..9a4e7af5a 100644 --- a/lib/configuration/variables/resolve-meta.js +++ b/lib/configuration/variables/resolve-meta.js @@ -1,4 +1,19 @@ -// Parse out variables meta for each configuration property +/* + * This module, resolve-meta.js, is responsible for parsing out variables meta + * for each configuration property in a Serverless service. + * + * It imports necessary dependencies and defines a function, parseEntries, which + * recursively traverses the configuration object. For each property, it checks + * the type of the value. If the value is an object or an array, it recursively + * calls parseEntries on the value. If the value is a string, it attempts to parse + * any variables in the string. If an error occurs during parsing, it adds an + * error message to the map for that property. + * + * The module exports a function that takes a configuration object as input. It + * ensures that the configuration is a plain object, and then calls parseEntries + * on the entries of the configuration object, initializing the map that will + * hold the parsed variables meta. + */ 'use strict'; diff --git a/lib/configuration/variables/resolve.js b/lib/configuration/variables/resolve.js index 74d2fc6b8..71f0eade6 100644 --- a/lib/configuration/variables/resolve.js +++ b/lib/configuration/variables/resolve.js @@ -1,4 +1,21 @@ -// Having variables meta, configuration, and sources setup, attempt to resolve all variables +/* + * This module, resolve.js, is responsible for resolving all variables in a given + * configuration. It's not used in the main Serverless process flow, but is + * useful for side resolution of variables. + * + * The module imports necessary dependencies and defines default sources for + * variable resolution. It also includes a function to report any errors that + * occur during variable resolution, throwing a ServerlessError with a detailed + * message if any errors are found. + * + * The resolveSourceValuesVariables function is a key part of this module. It + * handles the resolution of variables that come from multiple sources. It parses + * variables in each string part individually to avoid accidental resolution of + * variable-like notation which may surface after joining two values. + * + * The module exports an asynchronous function that takes a configuration object + * as input, which includes the service directory, service path, and configuration. + */ 'use strict'; @@ -98,15 +115,16 @@ class VariablesResolver { }) ) ) - ).then(() => {}); + ).then(() => { }); } return Promise.all( - Array.from(variablesMeta.keys(), (propertyPath) => - this.resolveProperty(resolutionBatchId, propertyPath) - ) - ).then(() => {}); + Array.from(variablesMeta.keys(), (propertyPath) => { + return this.resolveProperty(resolutionBatchId, propertyPath) + }) + ).then(() => { }); } + async resolveVariables(resolutionBatchId, propertyPath, valueMeta) { // Resolve all variables configured in given string value. await Promise.all( @@ -218,6 +236,7 @@ class VariablesResolver { variableMeta.error = error; return; } + if (resolvedData.value != null) { // Source successfully resolved. Accept as final value delete variableMeta.sources; @@ -332,9 +351,8 @@ class VariablesResolver { } catch (error) { error.message = `Cannot resolve variable at "${humanizePropertyPath( propertyPath.split('\0') - )}": Approached variable syntax error in resolved value "${valueMeta.value}": ${ - error.message - }`; + )}": Approached variable syntax error in resolved value "${valueMeta.value}": ${error.message + }`; delete valueMeta.value; valueMeta.error = error; throw error; diff --git a/lib/configuration/variables/sources/instance-dependent/get-cf.js b/lib/configuration/variables/sources/aws-cf.js similarity index 96% rename from lib/configuration/variables/sources/instance-dependent/get-cf.js rename to lib/configuration/variables/sources/aws-cf.js index 26a14dce2..8c161f0ea 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-cf.js +++ b/lib/configuration/variables/sources/aws-cf.js @@ -2,7 +2,7 @@ const ensureString = require('type/string/ensure'); const _ = require('lodash'); -const ServerlessError = require('../../../../serverless-error'); +const ServerlessError = require('../../../serverless-error'); module.exports = (serverlessInstance) => { return { diff --git a/lib/configuration/variables/sources/instance-dependent/get-s3.js b/lib/configuration/variables/sources/aws-s3.js similarity index 95% rename from lib/configuration/variables/sources/instance-dependent/get-s3.js rename to lib/configuration/variables/sources/aws-s3.js index 32a120446..6dd90510a 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-s3.js +++ b/lib/configuration/variables/sources/aws-s3.js @@ -1,7 +1,7 @@ 'use strict'; const ensureString = require('type/string/ensure'); -const ServerlessError = require('../../../../serverless-error'); +const ServerlessError = require('../../../serverless-error'); module.exports = (serverlessInstance) => { return { diff --git a/lib/configuration/variables/sources/instance-dependent/get-ssm.js b/lib/configuration/variables/sources/aws-ssm.js similarity index 97% rename from lib/configuration/variables/sources/instance-dependent/get-ssm.js rename to lib/configuration/variables/sources/aws-ssm.js index 89cc31dfd..e31a9c580 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-ssm.js +++ b/lib/configuration/variables/sources/aws-ssm.js @@ -1,7 +1,7 @@ 'use strict'; const ensureString = require('type/string/ensure'); -const ServerlessError = require('../../../../serverless-error'); +const ServerlessError = require('../../../serverless-error'); const _ = require('lodash'); module.exports = (serverlessInstance) => { diff --git a/lib/configuration/variables/sources/instance-dependent/get-aws.js b/lib/configuration/variables/sources/aws.js similarity index 83% rename from lib/configuration/variables/sources/instance-dependent/get-aws.js rename to lib/configuration/variables/sources/aws.js index 45b7a021d..be6afca79 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-aws.js +++ b/lib/configuration/variables/sources/aws.js @@ -1,7 +1,7 @@ 'use strict'; const ensureString = require('type/string/ensure'); -const ServerlessError = require('../../../../serverless-error'); +const ServerlessError = require('../../../serverless-error'); module.exports = (serverlessInstance) => { return { @@ -21,10 +21,10 @@ module.exports = (serverlessInstance) => { switch (address) { case 'accountId': { - const { Account } = await serverlessInstance + const { accountId } = await serverlessInstance .getProvider('aws') - .request('STS', 'getCallerIdentity', {}, { useCache: true }); - return { value: Account }; + .getCredentials(); + return { value: accountId }; } case 'region': { let region = options.region; diff --git a/lib/configuration/variables/sources/file.js b/lib/configuration/variables/sources/file.js index 021d591fb..0797be1fb 100644 --- a/lib/configuration/variables/sources/file.js +++ b/lib/configuration/variables/sources/file.js @@ -36,6 +36,7 @@ module.exports = { 'MISSING_FILE_SOURCE_PATH' ); } + const filePath = path.resolve( serviceDir, ensureString(params[0], { diff --git a/lib/configuration/variables/sources/opt.js b/lib/configuration/variables/sources/opt.js index 6a7af5c4a..ca2785603 100644 --- a/lib/configuration/variables/sources/opt.js +++ b/lib/configuration/variables/sources/opt.js @@ -11,6 +11,7 @@ module.exports = { errorMessage: 'Non-string address argument in variable "opt" source: %v', errorCode: 'INVALID_OPT_SOURCE_ADDRESS', }); + if (!isSourceFulfilled) { if (address == null) return { value: null, isPending: true }; if (options[address] !== undefined) return { value: options[address] }; diff --git a/lib/configuration/variables/sources/param.js b/lib/configuration/variables/sources/param.js new file mode 100644 index 000000000..352813a5f --- /dev/null +++ b/lib/configuration/variables/sources/param.js @@ -0,0 +1,92 @@ +'use strict'; + +const _ = require('lodash'); +const ensureString = require('type/string/ensure'); +const ServerlessError = require('../../../serverless-error'); + +module.exports = ({ + service = {}, + serviceInstanceParamsFromPlatform = {} +}) => { + return { + resolve: async ({ + address, + resolveConfigurationProperty, + options + }) => { + + if (!address) { + throw new ServerlessError( + 'Missing address argument in variable "param" source', + 'MISSING_PARAM_SOURCE_ADDRESS' + ); + } + address = ensureString(address, { + Error: ServerlessError, + errorMessage: 'Non-string address argument in variable "param" source: %v', + errorCode: 'INVALID_PARAM_SOURCE_ADDRESS', + }); + + const allParams = {}; + const stage = service.provider.stage; + + // Collect all Params from CLI options, stage params, and Serverless Platform + // If options.param is not an array, make it an array + if (options.param && !Array.isArray(options.param)) { + options.param = [options.param]; + } + if (options.param) { + const regex = /(?[^=]+)=(?.+)/; + for (const item of options.param) { + const res = item.match(regex); + if (!res) { + throw new ServerlessError( + `Encountered invalid "--param" CLI option value: "${item}". Supported format: "--param='='"`, + 'INVALID_CLI_PARAM_FORMAT' + ); + } + allParams[res.groups.key] = { value: res.groups.value.trimEnd(), type: 'cli' }; + } + } + const configParams = new Map( + Object.entries(_.get(service, 'params') || {}) + ); + // Collect all params with "default" + for (const [name, value] of new Map(Object.entries(configParams.get('default') || {}))) { + if (value == null) continue; + if (allParams[name] != null) continue; + allParams[name] = { value, type: 'configService' }; + } + // Collect all params from "stage" + for (const [name, value] of Object.entries(configParams.get(stage) || {})) { + if (value == null) continue; + // Overwrite default params + allParams[name] = { value, type: 'configServiceStage' }; + } + // Collect all params from serviceInstanceParamsFromPlatform + for (const [name, value] of Object.entries(serviceInstanceParamsFromPlatform)) { + if (value == null) continue; + if (allParams[name] != null) { + throw new ServerlessError( + `You have defined this parameter "${name}" in the Serverless Framework Dashboard as well as in your Service or CLI options. Please remove one of the definitions.`, + 'DUPLICATE_PARAM_FORMAT' + ); + } + allParams[name] = { value, type: 'dashboard' }; + } + + const result = { + value: allParams[address] ? + allParams[address].value : + null + }; + + if (allParams[address] == null) { + result.eventualErrorMessage = `The param "${address}" cannot be resolved from CLI options or stage params${service.org && service.app ? ' or Serverless Dashboard' : '' + }. If you are using Serverless Framework Compose, make sure to run commands via Compose so that all parameters can be resolved`; + } + + return result; + }, + }; +}; diff --git a/lib/configuration/variables/sources/resolve-unresolved-source-types.js b/lib/configuration/variables/sources/resolve-unresolved-source-types.js new file mode 100644 index 000000000..95c0865df --- /dev/null +++ b/lib/configuration/variables/sources/resolve-unresolved-source-types.js @@ -0,0 +1,28 @@ +'use strict'; + +const processVariables = (propertyPath, variablesMeta, resultMap) => { + if (!variablesMeta.variables) return; + for (const variableMeta of variablesMeta.variables) { + if (!variableMeta.sources) continue; + const sourceData = variableMeta.sources[0]; + if (!sourceData.type) continue; + if (sourceData.params) { + for (const paramData of sourceData.params) { + processVariables(propertyPath, paramData, resultMap); + } + } + if (sourceData.address) { + processVariables(propertyPath, sourceData.address, resultMap); + } + if (!resultMap.has(sourceData.type)) resultMap.set(sourceData.type, new Set()); + resultMap.get(sourceData.type).add(propertyPath); + } +}; + +module.exports = (propertiesVariablesMeta) => { + const resultMap = new Map(); + for (const [propertyPath, propertyVariablesMeta] of propertiesVariablesMeta) { + processVariables(propertyPath, propertyVariablesMeta, resultMap); + } + return resultMap; +}; diff --git a/lib/configuration/variables/sources/instance-dependent/get-sls.js b/lib/configuration/variables/sources/sls.js similarity index 94% rename from lib/configuration/variables/sources/instance-dependent/get-sls.js rename to lib/configuration/variables/sources/sls.js index b2c4832cf..9f1a81b28 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-sls.js +++ b/lib/configuration/variables/sources/sls.js @@ -1,7 +1,7 @@ 'use strict'; const ensureString = require('type/string/ensure'); -const ServerlessError = require('../../../../serverless-error'); +const ServerlessError = require('../../../serverless-error'); module.exports = (serverlessInstance) => { return { diff --git a/lib/serverless.js b/lib/serverless.js index d37287d3f..2ac17e110 100644 --- a/lib/serverless.js +++ b/lib/serverless.js @@ -30,6 +30,26 @@ const isStackFromOldLocalFallback = RegExp.prototype.test.bind( ); class Serverless { + + /** + * Constructs a new Serverless instance. This constructor is responsible for + * initializing various properties of the Serverless instance, including + * providers, version, access key, credential providers, service directory, + * commands, options, and various utility classes. It also handles validation + * of service path and filename input, and throws errors for incompatible + * configurations or outdated versions of the Framework. + * + * @param {Object} options - The options for the Serverless instance. + * @param {string} options.accessKey - The access key for the Serverless instance. + * @param {Array} options.commands - The commands for the Serverless instance. + * @param {Object} options.options - The options for the Serverless instance. + * @param {string} options.servicePath - The service path for the Serverless instance. + * @param {string} options.serviceConfigFileName - The service config file name. + * @param {Object} options.service - The service for the Serverless instance. + * @param {Object} options.credentialProviders - The credential providers. + * @param {Object} options.variablesMeta - The variables meta for the Serverless instance. + * @throws {ServerlessError} If there's an error in validation or incompatible configurations. + */ constructor({ accessKey = null, commands, @@ -39,6 +59,7 @@ class Serverless { service = {}, credentialProviders = {}, variablesMeta, + isDashboardEnabledForService = false, } = {}) { if (isStackFromOldLocalFallback(new Error().stack)) { @@ -64,17 +85,22 @@ class Serverless { ); } - // New root properties added in V.4 + this.providers = {}; + this.version = version; this.accessKey = accessKey || null; this.credentialProviders = credentialProviders; - this.isDashboardEnabled = !!(service.org && service.app); + this.isDashboardEnabledForService = isDashboardEnabledForService; + /** + * Validate Service path and filename input + */ this.serviceDir = ensureString(servicePath, { name: 'options.serviceDir', Error: ServerlessError, errorCode: 'INVALID_NON_STRING_SERVICE_DIR', isOptional: true, }); + if (this.serviceDir != null) { this.serviceDir = path.resolve(this.serviceDir); this.configurationFilename = ensureString(serviceConfigFileName, { @@ -96,8 +122,6 @@ class Serverless { }); } - this.providers = {}; - this.version = version; commands = ensureArray(commands); // Ensure that original `options` are not mutated, can be removed after addressing: // https://github.com/serverless/serverless/issues/2582 @@ -130,11 +154,19 @@ class Serverless { this.isStandaloneExecutable = isStandaloneExecutable; this.triggeredDeprecations = logDeprecation.triggeredDeprecations; this.isConfigurationExtendable = true; - - // TODO: Remove once "@serverless/dashboard-plugin" is integrated into this repository this._commandsSchema = commmandsSchema; } + /** + * Initializes the serverless instance. This method is responsible for creating + * an instance ID, initializing a new CLI instance, setting CLI options and + * commands, loading the service and all plugins, and setting the loaded plugins + * and commands for the CLI. This is typically called at the start of a + * serverless lifecycle. + * + * @async + * @throws {Error} If there's an error in loading the service or plugins. + */ async init() { // create an instanceId (can be e.g. used when a predictable random value is needed) this.instanceId = new Date().getTime().toString(); @@ -148,6 +180,7 @@ class Serverless { this.pluginManager.setCliCommands(this.processedInput.commands); await this.service.load(this.processedInput.options); + // load all plugins await this.pluginManager.loadAllPlugins(this.service.plugins); this.isConfigurationExtendable = false; @@ -157,6 +190,18 @@ class Serverless { this.cli.setLoadedCommands(this.pluginManager.getCommands()); } + /** + * Executes the Serverless instance. This method is responsible for reloading + * service file parameters, validating commands, setting variables, merging + * arrays, setting function names, validating the service configuration, and + * initializing service outputs. It also triggers the plugin lifecycle for + * processing commands. This method is typically called to run a Serverless + * instance. + * + * @async + * @throws {Error} If there's an error in command validation, service validation, + * or during the plugin lifecycle. + */ async run() { if (this.configurationInput) this.service.reloadServiceFileParam(); diff --git a/lib/utils/telemetry/anonymize-stacktrace-paths.js b/lib/utils/telemetry/anonymize-stacktrace-paths.js deleted file mode 100644 index b77f2244e..000000000 --- a/lib/utils/telemetry/anonymize-stacktrace-paths.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -const path = require('path'); -const commonPath = require('path2/common'); - -const anonymizeStacktracePaths = (stackFrames) => { - const stackFramesWithAbsolutePaths = stackFrames.filter((p) => path.isAbsolute(p)); - let commonPathPrefix = ''; - - if (stackFramesWithAbsolutePaths.length) { - commonPathPrefix = commonPath(...stackFramesWithAbsolutePaths); - - const lastServerlessPathIndex = commonPathPrefix.lastIndexOf( - `${path.sep}serverless${path.sep}` - ); - - if (lastServerlessPathIndex !== -1) { - commonPathPrefix = commonPathPrefix.slice(0, lastServerlessPathIndex); - } else { - const nodeModulesPathPart = `${path.sep}node_modules${path.sep}`; - const lastNodeModulesPathIndex = commonPathPrefix.lastIndexOf(nodeModulesPathPart); - if (lastNodeModulesPathIndex !== -1) { - commonPathPrefix = commonPathPrefix.slice( - 0, - lastNodeModulesPathIndex + nodeModulesPathPart.length - 1 - ); - } - } - } - - let previousStackFramePath = null; - return stackFrames.map((stackFrame) => { - stackFrame = stackFrame.replace(commonPathPrefix, ''); - const locationIndex = stackFrame.search(/:\d+:/); - if (locationIndex === -1) { - previousStackFramePath = null; - return stackFrame; - } - const currentStackFramePath = stackFrame.slice(0, locationIndex); - if (currentStackFramePath === previousStackFramePath) { - return `^${stackFrame.slice(currentStackFramePath.length)}`; - } - previousStackFramePath = currentStackFramePath; - return stackFrame; - }); -}; - -module.exports = anonymizeStacktracePaths; diff --git a/lib/utils/telemetry/are-disabled.js b/lib/utils/telemetry/are-disabled.js deleted file mode 100644 index f719f88b3..000000000 --- a/lib/utils/telemetry/are-disabled.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const configUtils = require('@serverless/utils/config'); - -module.exports = Boolean( - process.env.SLS_TELEMETRY_DISABLED || - process.env.SLS_TRACKING_DISABLED || - configUtils.get('trackingDisabled') -); diff --git a/lib/utils/telemetry/cache-path.js b/lib/utils/telemetry/cache-path.js deleted file mode 100644 index bff3e83d8..000000000 --- a/lib/utils/telemetry/cache-path.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const path = require('path'); -const os = require('os'); - -module.exports = (() => { - const resolvedHomeDir = os.homedir(); - if (!resolvedHomeDir) return null; - return path.resolve(resolvedHomeDir, '.serverless', 'telemetry-cache'); -})(); diff --git a/lib/utils/telemetry/generate-payload.js b/lib/utils/telemetry/generate-payload.js deleted file mode 100644 index eb0101c21..000000000 --- a/lib/utils/telemetry/generate-payload.js +++ /dev/null @@ -1,289 +0,0 @@ -'use strict'; - -const path = require('path'); -const crypto = require('crypto'); -const _ = require('lodash'); -const isPlainObject = require('type/plain-object/is'); -const isObject = require('type/object/is'); -const userConfig = require('@serverless/utils/config'); -const getNotificationsMode = require('@serverless/utils/get-notifications-mode'); -const log = require('@serverless/utils/log').log.get('telemetry'); -const isStandalone = require('../is-standalone-executable'); -const { getConfigurationValidationResult } = require('../../classes/config-schema-handler'); -const { triggeredDeprecations } = require('../log-deprecation'); -const isNpmGlobal = require('../npm-package/is-global'); -const isLocallyInstalled = require('../../cli/is-locally-installed'); -const ci = require('ci-info'); -const AWS = require('../../aws/sdk-v2'); - -const configValidationModeValues = new Set(['off', 'warn', 'error']); -const commandsReportingProjectId = new Set(['deploy', '', 'remove']); - -const getServiceConfig = ({ configuration, options, variableSources }) => { - const providerName = isObject(configuration.provider) - ? configuration.provider.name - : configuration.provider; - - const isAwsProvider = providerName === 'aws'; - - const defaultRuntime = isAwsProvider - ? configuration.provider.runtime || 'nodejs14.x' - : _.get(configuration, 'provider.runtime'); - - const functions = (() => { - if (isObject(configuration.functions)) return configuration.functions; - if (!Array.isArray(configuration.functions)) return {}; - const result = {}; - for (const functionsBlock of configuration.functions) Object.assign(result, functionsBlock); - return result; - })(); - - const resolveResourceTypes = (resources) => { - if (!isPlainObject(resources)) return []; - return [ - ...new Set( - Object.values(resources) - .map((resource) => { - const type = _.get(resource, 'Type'); - if (typeof type !== 'string' || !type.includes('::')) return null; - const domain = type.slice(0, type.indexOf(':')); - return domain === 'AWS' ? type : domain; - }) - .filter(Boolean) - ), - ]; - }; - - const resources = (() => { - if (!isAwsProvider) return undefined; - return { - general: resolveResourceTypes(_.get(configuration.resources, 'Resources')), - }; - })(); - - const plugins = configuration.plugins - ? configuration.plugins.modules || configuration.plugins - : []; - - const resolveUniqueParamsCount = (params) => { - if (!isPlainObject(params)) return 0; - return new Set( - Object.values(params) - .filter(isPlainObject) - .map((stageParams) => Object.keys(stageParams)) - .reduce((acc, stageParamsKeys) => [...acc, ...stageParamsKeys], []) - ).size; - }; - - const result = { - // TODO: Update when upgrading the default for next major - configValidationMode: configValidationModeValues.has(configuration.configValidationMode) - ? configuration.configValidationMode - : 'warn', - provider: { - name: providerName, - runtime: defaultRuntime, - stage: options.stage || _.get(configuration, 'provider.stage') || 'dev', - region: isAwsProvider - ? options.region || configuration.provider.region || 'us-east-1' - : _.get(configuration, 'provider.region'), - }, - variableSources: variableSources ? Array.from(variableSources) : [], - plugins, - functions: Object.values(functions) - .map((functionConfig) => { - if (!functionConfig) return null; - const functionEvents = Array.isArray(functionConfig.events) ? functionConfig.events : []; - const functionRuntime = (() => { - if (functionConfig.image) return '$containerimage'; - return functionConfig.runtime || defaultRuntime; - })(); - - return { - url: Boolean(functionConfig.url), - runtime: functionRuntime, - events: functionEvents.map((eventConfig) => ({ - type: isObject(eventConfig) ? Object.keys(eventConfig)[0] || null : null, - })), - }; - }) - .filter(Boolean), - resources, - paramsCount: resolveUniqueParamsCount(configuration.params), - }; - - // We want to recognize types of constructs from `serverless-lift` plugin if possible - if (plugins.includes('serverless-lift') && _.isObject(configuration.constructs)) { - result.constructs = Object.values(configuration.constructs) - .map((construct) => { - if (_.isObject(construct) && construct.type != null) { - return { type: construct.type }; - } - return null; - }) - .filter(Boolean); - } - - return result; -}; - -// This method is explicitly kept as synchronous. The reason for it being the fact that it needs to -// be executed in such manner due to its use in `process.on('SIGINT')` handler. -module.exports = ({ - command, - options, - commandSchema, - serviceDir, - configuration, - serverless, - commandUsage, - variableSources, - dockerVersion, -}) => { - let commandDurationMs; - - if (EvalError.$serverlessCommandStartTime) { - const diff = process.hrtime(EvalError.$serverlessCommandStartTime); - // First element is in seconds and second in nanoseconds - commandDurationMs = Math.floor(diff[0] * 1000 + diff[1] / 1000000); - } - - let timezone; - try { - timezone = new Intl.DateTimeFormat().resolvedOptions().timeZone; - } catch { - // Pass silently - } - - const ciName = (() => { - if (process.env.SERVERLESS_CI_CD) { - return 'Serverless CI/CD'; - } - - if (process.env.SEED_APP_NAME) { - return 'Seed'; - } - - if (ci.isCI) { - if (ci.name) { - return ci.name; - } - return 'unknown'; - } - return null; - })(); - - const userId = (() => { - // In this situation deployment relies on existence on company-wide access key - // and `userId` from config does not matter - if (process.env.SERVERLESS_ACCESS_KEY) { - return null; - } - - return userConfig.get('userId'); - })(); - - const usedVersions = (() => { - return { - 'serverless': require('../../../package').version, - }; - })(); - - // We only consider options that are present in command schema - const availableOptionNames = new Set(Object.keys(commandSchema.options)); - const commandOptionNames = Object.keys(options).filter((x) => availableOptionNames.has(x)); - - const payload = { - ciName, - isTtyTerminal: process.stdin.isTTY && process.stdout.isTTY, - isDockerInstalled: dockerVersion === undefined ? undefined : Boolean(dockerVersion), - dockerVersion: dockerVersion || undefined, - cliName: 'serverless', - command, - commandOptionNames, - dashboard: { - userId, - }, - firstLocalInstallationTimestamp: userConfig.get('meta.created_at'), - frameworkLocalUserId: userConfig.get('frameworkId'), - installationType: (() => { - if (isStandalone) { - if (process.platform === 'win32') return 'global:standalone:windows'; - return 'global:standalone:other'; - } - if (!isLocallyInstalled) { - return isNpmGlobal() ? 'global:npm' : 'global:other'; - } - if (EvalError.$serverlessInitInstallationVersion) return 'local:fallback'; - return 'local:direct'; - })(), - isAutoUpdateEnabled: Boolean(userConfig.get('autoUpdate.enabled')), - isUsingCompose: Boolean(process.env.SLS_COMPOSE), - notificationsMode: getNotificationsMode(), - timestamp: Date.now(), - timezone, - triggeredDeprecations: Array.from(triggeredDeprecations), - versions: usedVersions, - }; - - if (commandDurationMs != null) { - payload.commandDurationMs = commandDurationMs; - } - - if (configuration && commandSchema.serviceDependencyMode) { - const npmDependencies = (() => { - const pkgJson = (() => { - try { - return require(path.resolve(serviceDir, 'package.json')); - } catch (error) { - return null; - } - })(); - if (!pkgJson) return []; - return Array.from( - new Set([ - ...Object.keys(pkgJson.dependencies || {}), - ...Object.keys(pkgJson.optionalDependencies || {}), - ...Object.keys(pkgJson.devDependencies || {}), - ]) - ); - })(); - - const providerName = isObject(configuration.provider) - ? configuration.provider.name - : configuration.provider; - const isAwsProvider = providerName === 'aws'; - - payload.hasLocalCredentials = isAwsProvider && Boolean(new AWS.Config().credentials); - payload.npmDependencies = npmDependencies; - payload.config = getServiceConfig({ configuration, options, variableSources }); - payload.isConfigValid = getConfigurationValidationResult(configuration); - payload.dashboard.orgUid = - serverless && serverless.isDashboardEnabled ? serverless.service.orgUid : undefined; - if (isAwsProvider && serverless && commandsReportingProjectId.has(command)) { - const serviceName = isObject(configuration.service) - ? configuration.service.name - : configuration.service; - const accountId = serverless && serverless.getProvider('aws').accountId; - if (serviceName && accountId) { - payload.projectId = crypto - .createHash('sha256') - .update(`${serviceName}-${accountId}`) - .digest('base64'); - } - } - - if (isAwsProvider && serverless && (command === 'deploy' || command === '')) { - payload.didCreateService = Boolean( - serverless && serverless.getProvider('aws').didCreateService - ); - } - } - - if (commandUsage) { - payload.commandUsage = commandUsage; - } - - log.debug('payload %o', payload); - return payload; -}; diff --git a/lib/utils/telemetry/index.js b/lib/utils/telemetry/index.js deleted file mode 100644 index d9c202da6..000000000 --- a/lib/utils/telemetry/index.js +++ /dev/null @@ -1,195 +0,0 @@ -'use strict'; - -const { join } = require('path'); -const ensurePlainObject = require('type/plain-object/ensure'); -const { v1: uuid } = require('uuid'); -const fetch = require('node-fetch'); -const fse = require('fs-extra'); -const fsp = require('fs').promises; -const telemetryUrl = require('@serverless/utils/analytics-and-notfications-url'); -const { log } = require('@serverless/utils/log'); -const isTelemetryDisabled = require('./are-disabled'); -const cacheDirPath = require('./cache-path'); - -const telemetryLog = log.get('telemetry'); - -const timestampWeekBefore = Date.now() - 1000 * 60 * 60 * 24 * 7; - -const isUuid = RegExp.prototype.test.bind( - /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/ -); - -let serverlessRunEndTime; - -const logError = (type, error) => { - telemetryLog.debug('User stats error: %s: %O', type, error); - if (!process.env.SLS_STATS_DEBUG) return; -}; - -const markServerlessRunEnd = () => (serverlessRunEndTime = Date.now()); - -const processResponseBody = async (response, ids, startTime) => { - let result; - - try { - result = await response.json(); - } catch (error) { - logError(`Response processing error for ${ids || ''}`, error); - return null; - } - - const endTime = Date.now(); - if (serverlessRunEndTime) { - telemetryLog.debug( - 'Stats request prevented process from exiting for %dms (request time: %dms)', - endTime - serverlessRunEndTime, - endTime - startTime - ); - } - return result; -}; - -async function request(payload, { ids, timeout } = {}) { - const startTime = Date.now(); - let response; - const body = JSON.stringify(payload); - try { - response = await fetch(telemetryUrl, { - headers: { - 'content-type': 'application/json', - }, - method: 'POST', - // Ensure reasonable timeout to not block process from exiting - timeout: timeout || 3500, - body, - }); - } catch (networkError) { - logError('Request network error', networkError); - return null; - } - - if (response.status < 200 || response.status >= 300) { - logError('Unexpected request response', response); - return processResponseBody(response, ids, startTime); - } - - if (!ids) return processResponseBody(response, ids, startTime); - - await Promise.all( - ids.map(async (id) => { - const cachePath = join(cacheDirPath, id); - try { - await fsp.unlink(cachePath); - } catch (error) { - logError(`Could not remove cache file ${id}`, error); - } - }) - ); - - return processResponseBody(response, ids, startTime); -} - -// This method is explicitly kept as synchronous. The reason for it being the fact that it needs to -// be executed in such manner due to its use in `process.on('SIGINT')` handler. -function storeLocally(payload, options = {}) { - ensurePlainObject(payload); - if (!telemetryUrl) return null; - const isForced = options && options.isForced; - if (isTelemetryDisabled && !isForced) return null; - if (!cacheDirPath) return null; - const id = uuid(); - - return (function self() { - try { - // Additionally, we also append `id` to the payload to be used as $insert_id in Mixpanel to ensure event deduplication - return fse.writeJsonSync(join(cacheDirPath, id), { - payload: { ...payload, id }, - timestamp: Date.now(), - }); - } catch (error) { - if (error.code === 'ENOENT') { - try { - fse.ensureDirSync(cacheDirPath); - return self(); - } catch (ensureDirError) { - logError('Cache dir creation error:', ensureDirError); - } - } - logError(`Write cache file error: ${id}`, error); - return null; - } - })(); -} - -async function send(options = {}) { - const isForced = options && options.isForced; - serverlessRunEndTime = null; // Needed for testing - if (options.serverlessExecutionSpan) { - options.serverlessExecutionSpan.then(markServerlessRunEnd, markServerlessRunEnd); - } - if (isTelemetryDisabled && !isForced) return null; - if (!cacheDirPath) return null; - if (!telemetryUrl) return null; - let dirFilenames; - try { - dirFilenames = await fsp.readdir(cacheDirPath); - } catch (readdirError) { - if (readdirError.code !== 'ENOENT') logError('Cannot access cache dir', readdirError); - return null; - } - - const payloadsWithIds = ( - await Promise.all( - dirFilenames.map(async (dirFilename) => { - if (!isUuid(dirFilename)) return null; - let data; - try { - data = await fse.readJson(join(cacheDirPath, dirFilename)); - } catch (readJsonError) { - if (readJsonError.code === 'ENOENT') return null; // Race condition - logError(`Cannot read cache file: ${dirFilename}`, readJsonError); - const cacheFile = join(cacheDirPath, dirFilename); - try { - return await fsp.unlink(cacheFile); - } catch (error) { - logError(`Could not remove cache file ${dirFilename}`, error); - } - } - - if (data && data.payload) { - const timestamp = Number(data.timestamp); - if (timestamp > timestampWeekBefore) { - return { - payload: data.payload, - id: dirFilename, - }; - } - } else { - logError(`Invalid cached data ${dirFilename}`, data); - } - - const cacheFile = join(cacheDirPath, dirFilename); - try { - return await fsp.unlink(cacheFile); - } catch (error) { - logError(`Could not remove cache file ${dirFilename}`, error); - } - return null; - }) - ) - ).filter(Boolean); - - if (!payloadsWithIds.length) return null; - - return request( - payloadsWithIds - .map((item) => item.payload) - .sort((item, other) => item.timestamp - other.timestamp), - { - ids: payloadsWithIds.map((item) => item.id), - timeout: 3000, - } - ); -} - -module.exports = { storeLocally, send }; diff --git a/lib/utils/telemetry/resolve-error-location.js b/lib/utils/telemetry/resolve-error-location.js deleted file mode 100644 index 48255f771..000000000 --- a/lib/utils/telemetry/resolve-error-location.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const anonymizeStacktracePaths = require('./anonymize-stacktrace-paths'); - -const resolveErrorLocation = (exceptionTokens) => { - if (!exceptionTokens.stack) return ''; - - const splittedStack = exceptionTokens.stack.split(/[\r\n]+/); - if (splittedStack.length === 1 && exceptionTokens.code) return ''; - - const stacktraceLineRegex = /(?:\s*at.*\((.*:\d+:\d+)\).?|\s*at\s(.*:\d+:\d+))/; - const stacktracePaths = []; - for (const line of splittedStack) { - const match = line.match(stacktraceLineRegex) || []; - const matchedPath = match[1] || match[2]; - if (matchedPath) { - // Limited to maximum 7 lines in location - if (stacktracePaths.push(matchedPath) === 7) break; - } else if (stacktracePaths.length) break; - } - - if (!stacktracePaths.length) return ''; - - return anonymizeStacktracePaths(stacktracePaths).join('\n').replace(/\\/g, '/'); -}; - -module.exports = resolveErrorLocation; diff --git a/package.json b/package.json index 53e88cc43..d75b63d6d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "serverless", - "version": "3.38.0", - "description": "Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more", + "version": "4.0.0", + "description": "Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda", "preferGlobal": true, "homepage": "https://serverless.com/framework/docs/", "author": "serverless.com", @@ -18,8 +18,10 @@ ], "main": "lib/serverless.js", "dependencies": { - "@aws-sdk/client-sts": "^3.410.0", "@aws-sdk/client-cloudformation": "^3.410.0", + "@aws-sdk/client-ssm": "^3.501.0", + "@aws-sdk/client-sts": "^3.410.0", + "@aws-sdk/credential-provider-node": "^3.501.0", "@serverless/platform-client": "^4.5.1", "@serverless/utils": "^6.14.0", "abort-controller": "^3.0.0", @@ -228,7 +230,6 @@ "pkg:build": "node ./scripts/pkg/build.js", "pkg:generate-choco-package": "node ./scripts/pkg/generate-choco-package.js", "pkg:upload": "node ./scripts/pkg/upload/index.js", - "postinstall": "node ./scripts/postinstall.js", "prepare-release": "standard-version && prettier --write CHANGELOG.md", "prettier-check": "prettier -c \"**/*.{css,html,js,json,md,yaml,yml}\"", "prettier-check:updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml --base=main -- prettier -c", diff --git a/scripts/postinstall.js b/scripts/postinstall.js deleted file mode 100755 index 38ce6f2e5..000000000 --- a/scripts/postinstall.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -const chalk = require('chalk'); -const isStandaloneExecutable = require('../lib/utils/is-standalone-executable'); - -const isWindows = process.platform === 'win32'; - -const truthyStr = (val) => val && !['0', 'false', 'f', 'n', 'no'].includes(val.toLowerCase()); -const { CI, ADBLOCK, SILENT } = process.env; -const isNpmGlobalPackage = require('../lib/utils/npm-package/is-global'); - -if (!truthyStr(CI) && !truthyStr(ADBLOCK) && !truthyStr(SILENT)) { - const messageTokens = ['Serverless Framework successfully installed!']; - - if (isStandaloneExecutable && !isWindows) { - messageTokens.push( - 'To start your first project, please open another terminal and run “serverless”.' - ); - } else { - messageTokens.push('To start your first project run “serverless”.'); - } - - if ((isStandaloneExecutable && !isWindows) || isNpmGlobalPackage()) { - messageTokens.push('Turn on automatic updates by running “serverless config --autoupdate”.'); - } - - if (isStandaloneExecutable && !isWindows) { - messageTokens.push('Uninstall at any time by running “serverless uninstall”.'); - } - - process.stdout.write(`${chalk.grey(messageTokens.join('\n\n'))}\n`); -} diff --git a/scripts/serverless.js b/scripts/serverless.js deleted file mode 100755 index bed72a910..000000000 --- a/scripts/serverless.js +++ /dev/null @@ -1,853 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -require('essentials'); - -// global graceful-fs patch -// https://github.com/isaacs/node-graceful-fs#global-patching -require('graceful-fs').gracefulify(require('fs')); - -// Setup log writing -require('@serverless/utils/log-reporters/node'); -const { log, progress, isInteractive: isInteractiveTerminal } = require('@serverless/utils/log'); - -const processLog = log.get('process'); - -const handleError = require('../lib/cli/handle-error'); -const { - storeLocally: storeTelemetryLocally, - send: sendTelemetry, -} = require('../lib/utils/telemetry'); -const generateTelemetryPayload = require('../lib/utils/telemetry/generate-payload'); -const isTelemetryDisabled = require('../lib/utils/telemetry/are-disabled'); -const logDeprecation = require('../lib/utils/log-deprecation'); - -let command; -let isHelpRequest; -let options; -let commandSchema; -let serviceDir = null; -let configuration = null; -let serverless; -let dockerVersion; -const commandUsage = {}; -const variableSourcesInConfig = new Set(); - -// Inquirer async operations do not keep node process alive -// We need to issue a keep alive timer so process does not die -// to properly handle e.g. `SIGINT` interrupt -const keepAliveTimer = setTimeout(() => {}, 60 * 60 * 1000); - -const trueWithProbability = (probability) => Math.random() < probability; - -let processSpanPromise; -let hasBeenFinalized = false; -const finalize = async ({ error, shouldBeSync, telemetryData, shouldSendTelemetry } = {}) => { - processLog.debug('finalize %o', { error, shouldBeSync, telemetryData, shouldSendTelemetry }); - if (hasBeenFinalized) { - if (error) { - // Programmer error in finalize handling, ensure to expose - process.nextTick(() => { - throw error; - }); - } - return null; - } - hasBeenFinalized = true; - clearTimeout(keepAliveTimer); - progress.clear(); - if (error) ({ telemetryData } = await handleError(error, { serverless })); - if (!shouldBeSync) { - await logDeprecation.printSummary(); - } - if (isTelemetryDisabled || !commandSchema) return null; - if (!error && isHelpRequest) return null; - storeTelemetryLocally({ - ...generateTelemetryPayload({ - command, - options, - commandSchema, - serviceDir, - configuration, - serverless, - commandUsage, - variableSources: variableSourcesInConfig, - dockerVersion, - }), - ...telemetryData, - }); - - // We want to explicitly ensure that when processing should be sync, we never attempt sending telemetry data - if (shouldBeSync) return null; - - // We want to send telemetry at least roughly every 20 commands (in addition to sending on deploy and on errors) - // to avoid situations where we have very big batches of telemetry events that cannot be processed on the backend side - const shouldForceTelemetry = trueWithProbability(0.05); - - if (!error && !shouldSendTelemetry && !shouldForceTelemetry) return null; - return sendTelemetry({ serverlessExecutionSpan: processSpanPromise }); -}; - -process.once('uncaughtException', (error) => { - log.error('Uncaught exception'); - finalize({ error }).then(() => process.exit()); -}); - -processSpanPromise = (async () => { - try { - const wait = require('timers-ext/promise/sleep'); - await wait(); // Ensure access to "processSpanPromise" - - require('signal-exit/signals').forEach((signal) => { - process.once(signal, () => { - processLog.debug('exit signal %s', signal); - // If there's another listener (e.g. we're in daemon context or reading stdin input) - // then let the other listener decide how process will exit - const isOtherSigintListener = Boolean(process.listenerCount(signal)); - finalize({ - shouldBeSync: true, - telemetryData: { outcome: 'interrupt', interruptSignal: signal }, - }); - if (isOtherSigintListener) return; - // Follow recommendation from signal-exit: - // https://github.com/tapjs/signal-exit/blob/654117d6c9035ff6a805db4d4acf1f0c820fcb21/index.js#L97-L98 - if (process.platform === 'win32' && signal === 'SIGHUP') signal = 'SIGINT'; - process.kill(process.pid, signal); - }); - }); - - const humanizePropertyPathKeys = require('../lib/configuration/variables/humanize-property-path-keys'); - const processBackendNotificationRequest = require('../lib/utils/process-backend-notification-request'); - - (() => { - // Rewrite eventual `sls deploy -f` into `sls deploy function -f` - // Also rewrite `serverless dev` to `serverless --dev`` - const isParamName = RegExp.prototype.test.bind(require('../lib/cli/param-reg-exp')); - - const args = process.argv.slice(2); - const firstParamIndex = args.findIndex(isParamName); - const commands = args.slice(0, firstParamIndex === -1 ? Infinity : firstParamIndex); - - if (commands.join('') === 'dev') { - process.argv[2] = '--dev'; - return; - } - - if (commands.join(' ') !== 'deploy') return; - if (!args.includes('-f') && !args.includes('--function')) return; - logDeprecation( - 'CLI_DEPLOY_FUNCTION_OPTION_V3', - 'Starting with v4.0.0, `--function` or `-f` option for `deploy` command will no longer be supported. In order to deploy a single function, please use `deploy function` command instead.' - ); - process.argv.splice(3, 0, 'function'); - })(); - - const resolveInput = require('../lib/cli/resolve-input'); - - let commands; - processLog.debug('resolve CLI input (no service schema)'); - // Parse args against schemas of commands which do not require to be run in service context - ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( - require('../lib/cli/commands-schema/no-service') - )); - - // If version number request, show it and abort - if (options.version) { - processLog.debug('render version'); - await require('../lib/cli/render-version')(); - await finalize(); - return; - } - - const ServerlessError = require('../lib/serverless-error'); - - // Abort if command is not supported in this environment - if (commandSchema && commandSchema.isHidden && commandSchema.noSupportNotice) { - throw new ServerlessError( - `Cannot run \`${command}\` command: ${commandSchema.noSupportNotice}`, - 'NOT_SUPPORTED_COMMAND' - ); - } - - const path = require('path'); - const uuid = require('uuid'); - const _ = require('lodash'); - const clear = require('ext/object/clear'); - const Serverless = require('../lib/serverless'); - const resolveVariables = require('../lib/configuration/variables/resolve'); - const isPropertyResolved = require('../lib/configuration/variables/is-property-resolved'); - const eventuallyReportVariableResolutionErrors = require('../lib/configuration/variables/eventually-report-resolution-errors'); - const filterSupportedOptions = require('../lib/cli/filter-supported-options'); - const isDashboardEnabled = require('../lib/configuration/is-dashboard-enabled'); - - let configurationPath = null; - let providerName; - let variablesMeta; - let resolverConfiguration; - let isInteractiveSetup; - - const ensureResolvedProperty = (propertyPath) => { - if (isPropertyResolved(variablesMeta, propertyPath)) return true; - variablesMeta = null; - if (isHelpRequest) return false; - const humanizedPropertyPath = humanizePropertyPathKeys(propertyPath.split('\0')); - throw new ServerlessError( - `Cannot resolve ${path.basename( - configurationPath - )}: "${humanizedPropertyPath}" property is not accessible ` + - '(configured behind variables which cannot be resolved at this stage)', - 'INACCESSIBLE_CONFIGURATION_PROPERTY' - ); - }; - - if (!commandSchema || commandSchema.serviceDependencyMode) { - // Command is potentially service specific, follow up with resolution of service config - - // Parse args again, taking account schema of service-specific flags - // as they may influence configuration resolution - processLog.debug('resolve CLI input (service schema)'); - resolveInput.clear(); - ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( - require('../lib/cli/commands-schema/service') - )); - - isInteractiveSetup = !isHelpRequest && command === ''; - - processLog.debug('resolve eventual service configuration'); - const resolveConfigurationPath = require('../lib/cli/resolve-configuration-path'); - const readConfiguration = require('../lib/configuration/read'); - const resolveProviderName = require('../lib/configuration/resolve-provider-name'); - - // Resolve eventual service configuration path - configurationPath = await resolveConfigurationPath(); - if (configurationPath) { - processLog.debug('service configuration found at %s', configurationPath); - } else processLog.debug('no service configuration found'); - - // If service configuration file is found, load its content - configuration = configurationPath - ? await (async () => { - try { - return await readConfiguration(configurationPath); - } catch (error) { - // Configuration syntax error should not prevent help from being displayed - // (if possible configuration should be read for help request as registered - // plugins may introduce new commands to be listed in help output) - if (isHelpRequest) return null; - throw error; - } - })() - : null; - - if (configuration) { - processLog.debug('service configuration file successfully parsed'); - serviceDir = process.cwd(); - - // IIFE for maintenance convenience - await (async () => { - // We do not need to attempt resolution of further variables for login command as - // the only variables from configuration that we potentially rely on is `app` and `org` - // TODO: Remove when dashboard/console login prompt won't be needed - when that happens - // login command should once again be service independent - if (command === 'login') return; - - processLog.debug('resolve variables meta'); - const resolveVariablesMeta = require('../lib/configuration/variables/resolve-meta'); - - variablesMeta = resolveVariablesMeta(configuration); - - if ( - eventuallyReportVariableResolutionErrors( - configurationPath, - configuration, - variablesMeta - ) - ) { - // Variable syntax errors, abort - variablesMeta = null; - return; - } - - if (!ensureResolvedProperty('disabledDeprecations')) return; - if (!ensureResolvedProperty('deprecationNotificationMode')) return; - - if (isPropertyResolved(variablesMeta, 'provider\0name')) { - providerName = resolveProviderName(configuration); - if (providerName == null) { - variablesMeta = null; - return; - } - } - if (!commandSchema && providerName === 'aws') { - // If command was not recognized in previous resolution phases - // parse args again also against schemas commands which require AWS service context - processLog.debug('resolve CLI input (AWS service schema)'); - resolveInput.clear(); - ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( - require('../lib/cli/commands-schema/aws-service') - )); - } - - let envVarNamesNeededForDotenvResolution; - if (variablesMeta.size) { - processLog.debug('resolve variables in core properties'); - // Some properties are configured with variables - - // Resolve eventual variables in `provider.stage` and `useDotEnv` - // (required for reliable .env resolution) - resolverConfiguration = { - serviceDir, - configuration, - variablesMeta, - sources: { - env: require('../lib/configuration/variables/sources/env'), - file: require('../lib/configuration/variables/sources/file'), - opt: require('../lib/configuration/variables/sources/opt'), - self: require('../lib/configuration/variables/sources/self'), - strToBool: require('../lib/configuration/variables/sources/str-to-bool'), - sls: require('../lib/configuration/variables/sources/instance-dependent/get-sls')(), - }, - options: filterSupportedOptions(options, { commandSchema, providerName }), - fulfilledSources: new Set(['file', 'self', 'strToBool']), - propertyPathsToResolve: new Set(['provider\0name', 'provider\0stage', 'useDotenv']), - variableSourcesInConfig, - }; - - if (isInteractiveSetup) resolverConfiguration.fulfilledSources.add('opt'); - await resolveVariables(resolverConfiguration); - - if ( - eventuallyReportVariableResolutionErrors( - configurationPath, - configuration, - variablesMeta - ) - ) { - // Unrecoverable resolution errors, abort - variablesMeta = null; - return; - } - - if (!providerName && isPropertyResolved(variablesMeta, 'provider\0name')) { - providerName = resolveProviderName(configuration); - if (providerName == null) { - variablesMeta = null; - return; - } - if (!commandSchema && providerName === 'aws') { - // If command was not recognized in previous resolution phases - // Parse args again also against schemas of commands which work in context of an AWS - // service - processLog.debug('resolve CLI input (AWS service schema)'); - resolveInput.clear(); - ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( - require('../lib/cli/commands-schema/aws-service') - )); - - if (commandSchema) { - processLog.debug('resolve variables in core properties #2'); - resolverConfiguration.options = filterSupportedOptions(options, { - commandSchema, - providerName, - }); - await resolveVariables(resolverConfiguration); - if ( - eventuallyReportVariableResolutionErrors( - configurationPath, - configuration, - variablesMeta - ) - ) { - variablesMeta = null; - return; - } - } - } - } - - resolverConfiguration.fulfilledSources.add('env'); - if ( - !isPropertyResolved(variablesMeta, 'provider\0stage') || - !isPropertyResolved(variablesMeta, 'useDotenv') - ) { - // Assume "env" source fulfilled for `provider.stage` and `useDotenv` resolution. - // To pick eventual resolution conflict, track what env variables were reported - // missing when applying this resolution - processLog.debug('resolve variables in stage related properties'); - const envSource = require('../lib/configuration/variables/sources/env'); - envSource.missingEnvVariables.clear(); - await resolveVariables({ - ...resolverConfiguration, - propertyPathsToResolve: new Set(['provider\0stage', 'useDotenv']), - }); - if ( - eventuallyReportVariableResolutionErrors( - configurationPath, - configuration, - variablesMeta - ) - ) { - // Unrecoverable resolution errors, abort - variablesMeta = null; - return; - } - - if ( - !ensureResolvedProperty('provider\0stage', { - shouldSilentlyReturnIfLegacyMode: true, - }) - ) { - return; - } - - if (!ensureResolvedProperty('useDotenv')) return; - - envVarNamesNeededForDotenvResolution = envSource.missingEnvVariables; - } - } - - // Load eventual environment variables from .env files - if (await require('../lib/cli/conditionally-load-dotenv')(options, configuration)) { - if (envVarNamesNeededForDotenvResolution) { - for (const envVarName of envVarNamesNeededForDotenvResolution) { - if (process.env[envVarName]) { - throw new ServerlessError( - 'Cannot reliably resolve "env" variables due to resolution conflict.\n' + - `Environment variable "${envVarName}" which influences resolution of ` + - '".env" file were found to be defined in resolved ".env" file.' + - 'DOTENV_ENV_VAR_RESOLUTION_CONFLICT' - ); - } - } - } - if (!isPropertyResolved(variablesMeta, 'provider\0name')) { - processLog.debug('resolve variables in "provider.name"'); - await resolveVariables(resolverConfiguration); - if ( - eventuallyReportVariableResolutionErrors( - configurationPath, - configuration, - variablesMeta - ) - ) { - variablesMeta = null; - return; - } - } - } - - if (!variablesMeta.size) return; // No properties configured with variables - - if (!providerName) { - if (!ensureResolvedProperty('provider\0name')) return; - providerName = resolveProviderName(configuration); - if (providerName == null) { - variablesMeta = null; - return; - } - if (!commandSchema && providerName === 'aws') { - processLog.debug('resolve CLI input (AWS service schema)'); - resolveInput.clear(); - ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( - require('../lib/cli/commands-schema/aws-service') - )); - if (commandSchema) { - resolverConfiguration.options = filterSupportedOptions(options, { - commandSchema, - providerName, - }); - } - } - } - if (isHelpRequest || commands[0] === 'plugin') { - processLog.debug('resolve variables in "plugins"'); - // We do not need full config resolved, we just need to know what - // provider is service setup with, and with what eventual plugins Framework is extended - // as that influences what CLI commands and options could be used, - resolverConfiguration.propertyPathsToResolve.add('plugins'); - } else { - processLog.debug('resolve variables in all properties'); - delete resolverConfiguration.propertyPathsToResolve; - } - - await resolveVariables(resolverConfiguration); - if ( - eventuallyReportVariableResolutionErrors( - configurationPath, - configuration, - variablesMeta - ) - ) { - variablesMeta = null; - return; - } - - if (!variablesMeta.size) return; // All properties successfully resolved - - if (!ensureResolvedProperty('plugins')) return; - - // At this point we have all properties needed for `plugin install/uninstall` commands - if (commands[0] === 'plugin') { - return; - } - - if (!ensureResolvedProperty('package\0path')) return; - - if (!ensureResolvedProperty('frameworkVersion')) return; - if (!ensureResolvedProperty('app')) return; - if (!ensureResolvedProperty('org')) return; - if (!ensureResolvedProperty('dashboard')) return; - if (!ensureResolvedProperty('service')) return; - if (isDashboardEnabled({ configuration, options })) { - // Dashboard requires AWS region to be resolved upfront - ensureResolvedProperty('provider\0region'); - } - })(); - - // Ensure to have full AWS commands schema loaded if we're in context of AWS provider - // It's not the case if not AWS service specific command was resolved - if (configuration && resolveProviderName(configuration) === 'aws') { - processLog.debug('resolve CLI input (AWS service schema)'); - resolveInput.clear(); - ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( - require('../lib/cli/commands-schema/aws-service') - )); - } - } else { - // In non-service context we recognize all AWS service commands - processLog.debug('parsing of configuration file failed'); - processLog.debug('resolve CLI input (AWS service schema)'); - resolveInput.clear(); - ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( - require('../lib/cli/commands-schema/aws-service') - )); - - // Validate result command and options - require('../lib/cli/ensure-supported-command')(); - } - } else { - require('../lib/cli/ensure-supported-command')(); - } - - const configurationFilename = configuration && configurationPath.slice(serviceDir.length + 1); - - // Names of the commands which are configured independently in root `commands` folder - // and not in Serverless class internals - const notIntegratedCommands = new Set([ - 'doctor', - 'login', - 'logout', - 'plugin install', - 'plugin uninstall', - ]); - const isStandaloneCommand = notIntegratedCommands.has(command); - - if (!isHelpRequest) { - if (isStandaloneCommand) { - processLog.debug('run standalone command'); - if (configuration) require('../lib/cli/ensure-supported-command')(configuration); - await require(`../commands/${commands.join('-')}`)({ - configuration, - serviceDir, - configurationFilename, - options, - }); - await finalize({ telemetryData: { outcome: 'success' } }); - return; - } else if (isInteractiveSetup) { - if (!isInteractiveTerminal) { - throw new ServerlessError( - 'Attempted to run an interactive setup in non TTY environment.\n' + - "If that's intended, run with the SLS_INTERACTIVE_SETUP_ENABLE=1 environment variable", - 'INTERACTIVE_SETUP_IN_NON_TTY' - ); - } - if (!configuration) { - processLog.debug('run interactive onboarding'); - const interactiveContext = await require('../lib/cli/interactive-setup')({ - configuration, - serviceDir, - configurationFilename, - options, - commandUsage, - }); - if (interactiveContext.configuration) { - configuration = interactiveContext.configuration; - } - if (interactiveContext.serverless) { - serverless = interactiveContext.serverless; - } - await finalize({ telemetryData: { outcome: 'success' }, shouldSendTelemetry: true }); - return; - } - } - } - - processLog.debug('construct Serverless instance'); - serverless = new Serverless({ - configuration, - serviceDir, - configurationFilename, - commands, - options, - variablesMeta, - }); - - try { - serverless.onExitPromise = processSpanPromise; - serverless.invocationId = uuid.v4(); - processLog.debug('initialize Serverless instance'); - await serverless.init(); - - // IIFE for maintenance convenience - await (async () => { - if (!configuration) return; - - let hasFinalCommandSchema = false; - if (configuration.plugins) { - // After plugins are loaded, re-resolve CLI command and options schema as plugin - // might have defined extra commands and options - - if (serverless.pluginManager.externalPlugins.size) { - processLog.debug('resolve CLI input (+ plugins schema)'); - const commandsSchema = require('../lib/cli/commands-schema/resolve-final')( - serverless.pluginManager.externalPlugins, - { providerName: providerName || 'aws', configuration } - ); - resolveInput.clear(); - ({ command, commands, options, isHelpRequest, commandSchema } = - resolveInput(commandsSchema)); - serverless.processedInput.commands = serverless.pluginManager.cliCommands = commands; - serverless.processedInput.options = options; - Object.assign(clear(serverless.pluginManager.cliOptions), options); - hasFinalCommandSchema = true; - } - } - if (!providerName && !hasFinalCommandSchema) { - // Invalid configuration, ensure to recognize all AWS commands - processLog.debug('resolve CLI input (AWS service schema)'); - resolveInput.clear(); - ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( - require('../lib/cli/commands-schema/aws-service') - )); - } - hasFinalCommandSchema = true; - - // Validate result command and options - if (hasFinalCommandSchema) require('../lib/cli/ensure-supported-command')(configuration); - if (isHelpRequest) return; - if (!_.get(variablesMeta, 'size')) return; - if (!resolverConfiguration) { - // There were no variables in the initial configuration, yet it was extended by - // the plugins with ones. - // In this case we need to ensure `resolverConfiguration` which initially was not setup - resolverConfiguration = { - serviceDir, - configuration, - variablesMeta, - sources: { - env: require('../lib/configuration/variables/sources/env'), - file: require('../lib/configuration/variables/sources/file'), - opt: require('../lib/configuration/variables/sources/opt'), - self: require('../lib/configuration/variables/sources/self'), - strToBool: require('../lib/configuration/variables/sources/str-to-bool'), - sls: require('../lib/configuration/variables/sources/instance-dependent/get-sls')(), - }, - options: filterSupportedOptions(options, { commandSchema, providerName }), - fulfilledSources: new Set(['env', 'file', 'self', 'strToBool']), - propertyPathsToResolve: - commands[0] === 'plugin' - ? new Set(['plugins', 'provider\0name', 'provider\0stage', 'useDotenv']) - : null, - variableSourcesInConfig, - }; - } - - if (commandSchema) { - resolverConfiguration.options = filterSupportedOptions(options, { - commandSchema, - providerName, - }); - } - resolverConfiguration.fulfilledSources.add('opt'); - - // Register serverless instance specific variable sources - resolverConfiguration.sources.sls = - require('../lib/configuration/variables/sources/instance-dependent/get-sls')(serverless); - resolverConfiguration.fulfilledSources.add('sls'); - - resolverConfiguration.sources.param = - serverless.pluginManager.dashboardPlugin.configurationVariablesSources.param; - resolverConfiguration.fulfilledSources.add('param'); - - // Register dashboard specific variable source resolvers - if (isDashboardEnabled({ configuration, options })) { - for (const [sourceName, sourceConfig] of Object.entries( - serverless.pluginManager.dashboardPlugin.configurationVariablesSources - )) { - if (sourceName === 'param') continue; - resolverConfiguration.sources[sourceName] = sourceConfig; - resolverConfiguration.fulfilledSources.add(sourceName); - } - } - - // Register AWS provider specific variable sources - if (providerName === 'aws') { - // Pre-resolve to eventually pick not yet resolved AWS auth related properties - processLog.debug('resolve variables'); - await resolveVariables(resolverConfiguration); - if (!variablesMeta.size) return; - if ( - eventuallyReportVariableResolutionErrors( - configurationPath, - configuration, - variablesMeta - ) - ) { - return; - } - - // Ensure properties which are crucial to some variable source resolvers - // are actually resolved. - if ( - !ensureResolvedProperty('provider\0credentials') || - !ensureResolvedProperty('provider\0deploymentBucket\0serverSideEncryption') || - !ensureResolvedProperty('provider\0profile') || - !ensureResolvedProperty('provider\0region') - ) { - return; - } - Object.assign(resolverConfiguration.sources, { - cf: require('../lib/configuration/variables/sources/instance-dependent/get-cf')( - serverless - ), - s3: require('../lib/configuration/variables/sources/instance-dependent/get-s3')( - serverless - ), - ssm: require('../lib/configuration/variables/sources/instance-dependent/get-ssm')( - serverless - ), - aws: require('../lib/configuration/variables/sources/instance-dependent/get-aws')( - serverless - ), - }); - resolverConfiguration.fulfilledSources.add('cf').add('s3').add('ssm').add('aws'); - } - - // Register variable source resolvers provided by external plugins - const resolverExternalPluginSources = require('../lib/configuration/variables/sources/resolve-external-plugin-sources'); - resolverExternalPluginSources( - configuration, - resolverConfiguration, - serverless.pluginManager.externalPlugins - ); - - // Having all source resolvers configured, resolve variables - processLog.debug('resolve all variables'); - await resolveVariables(resolverConfiguration); - if (!variablesMeta.size) return; - if ( - eventuallyReportVariableResolutionErrors(configurationPath, configuration, variablesMeta) - ) { - return; - } - - // Do not confirm on unresolved sources with partially resolved configuration - if (resolverConfiguration.propertyPathsToResolve) return; - - processLog.debug('uresolved variables meta: %o', variablesMeta); - // Report unrecognized variable sources found in variables configured in service config - const unresolvedSources = - require('../lib/configuration/variables/resolve-unresolved-source-types')(variablesMeta); - const recognizedSourceNames = new Set(Object.keys(resolverConfiguration.sources)); - - const unrecognizedSourceNames = Array.from(unresolvedSources.keys()).filter( - (sourceName) => !recognizedSourceNames.has(sourceName) - ); - - if (unrecognizedSourceNames.includes('output')) { - throw new ServerlessError( - '"Cannot resolve configuration: ' + - '"output" variable can only be used in ' + - 'services deployed with Serverless Dashboard (with "org" setting configured)', - 'DASHBOARD_VARIABLE_SOURCES_MISUSE' - ); - } - throw new ServerlessError( - `Unrecognized configuration variable sources: "${unrecognizedSourceNames.join('", "')}"`, - 'UNRECOGNIZED_VARIABLE_SOURCES' - ); - })(); - - if (isHelpRequest && serverless.pluginManager.externalPlugins) { - // Show help - processLog.debug('render help'); - require('../lib/cli/render-help')(serverless.pluginManager.externalPlugins); - } else if (isInteractiveSetup) { - processLog.debug('run interactive onboarding'); - const interactiveContext = await require('../lib/cli/interactive-setup')({ - configuration, - serverless, - serviceDir, - configurationFilename, - options, - commandUsage, - }); - if (interactiveContext.configuration) { - configuration = interactiveContext.configuration; - } - if (interactiveContext.serverless) { - serverless = interactiveContext.serverless; - } - } else { - if (commands.join(' ') === 'deploy') { - const spawn = require('child-process-ext/spawn'); - spawn('docker', ['--version']).then( - ({ stdoutBuffer }) => { - dockerVersion = null; - const matcher = String(stdoutBuffer).match(/(?\d+\.\d+\.\d+),/); - if (!matcher) return; - dockerVersion = matcher.groups.version; - }, - () => (dockerVersion = null) - ); - } - - processLog.debug('run Serverless instance'); - // Run command - await serverless.run(); - } - - const backendNotificationRequest = await finalize({ - telemetryData: { outcome: 'success' }, - shouldSendTelemetry: isInteractiveSetup || commands.join(' ') === 'deploy', - }); - if (!isInteractiveSetup && backendNotificationRequest) { - await processBackendNotificationRequest(backendNotificationRequest); - } - } catch (error) { - processLog.debug('handle error'); - // If Dashboard Plugin, capture error - const dashboardPlugin = serverless.pluginManager.dashboardPlugin; - const dashboardErrorHandler = _.get(dashboardPlugin, 'enterprise.errorHandler'); - if (!dashboardErrorHandler) throw error; - try { - await dashboardErrorHandler(error, serverless.invocationId); - } catch (dashboardErrorHandlerError) { - const tokenizeException = require('../lib/utils/tokenize-exception'); - const exceptionTokens = tokenizeException(dashboardErrorHandlerError); - log.warning( - `Publication to Serverless Dashboard errored with:\n${' '.repeat('Serverless: '.length)}${ - exceptionTokens.isUserError || !exceptionTokens.stack - ? exceptionTokens.message - : exceptionTokens.stack - }` - ); - } - throw error; - } - } catch (error) { - await finalize({ error }); - } -})();