chore: remove unnecessary files and rework variable resolution (#12340)

This commit is contained in:
Austen 2024-01-31 14:50:04 -08:00 committed by GitHub
parent 347d913c19
commit 4fa39b8a80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1220 additions and 2062 deletions

View File

@ -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);
}
}
/**

View File

@ -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',

View File

@ -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',
},
};

View File

@ -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',
},
},
});

View File

@ -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';
}

View File

@ -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';
}

View File

@ -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';
}

View File

@ -1,3 +0,0 @@
'use strict';
module.exports = require('./aws-service');

View File

@ -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' };
}
}

View File

@ -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;
};

View File

@ -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);

View File

@ -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];

View File

@ -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 { };
};

View File

@ -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';

View File

@ -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');

View File

@ -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);

View File

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

View File

@ -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;
};

View File

@ -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);

View File

@ -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,
};

View File

@ -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';

View File

@ -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: {} });

View File

@ -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';

View File

@ -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';

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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) => {

View File

@ -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;

View File

@ -36,6 +36,7 @@ module.exports = {
'MISSING_FILE_SOURCE_PATH'
);
}
const filePath = path.resolve(
serviceDir,
ensureString(params[0], {

View File

@ -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] };

View File

@ -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 = /(?<key>[^=]+)=(?<value>.+)/;
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='<key>=<val>'"`,
'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;
},
};
};

View File

@ -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;
};

View File

@ -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 {

View File

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

View File

@ -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;

View File

@ -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')
);

View File

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

View File

@ -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;
};

View File

@ -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 || '<no id>'}`, 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 };

View File

@ -1,27 +0,0 @@
'use strict';
const anonymizeStacktracePaths = require('./anonymize-stacktrace-paths');
const resolveErrorLocation = (exceptionTokens) => {
if (!exceptionTokens.stack) return '<not accessible due to non-error exception>';
const splittedStack = exceptionTokens.stack.split(/[\r\n]+/);
if (splittedStack.length === 1 && exceptionTokens.code) return '<not available>';
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 '<not reflected in stack>';
return anonymizeStacktracePaths(stacktracePaths).join('\n').replace(/\\/g, '/');
};
module.exports = resolveErrorLocation;

View File

@ -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",

View File

@ -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`);
}

View File

@ -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(/(?<version>\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 });
}
})();