mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
265 lines
10 KiB
JavaScript
265 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const _ = require('lodash');
|
|
const ensureString = require('type/string/ensure');
|
|
const ensureValue = require('type/value/ensure');
|
|
const ensureArray = require('type/array/ensure');
|
|
const ensureIterable = require('type/iterable/ensure');
|
|
const isPlainObject = require('type/plain-object/is');
|
|
const ensurePlainObject = require('type/plain-object/ensure');
|
|
const CLI = require('./classes/cli');
|
|
const Config = require('./classes/config');
|
|
const YamlParser = require('./classes/yaml-parser');
|
|
const PluginManager = require('./classes/plugin-manager');
|
|
const Utils = require('./classes/utils');
|
|
const Service = require('./classes/service');
|
|
const ConfigSchemaHandler = require('./classes/config-schema-handler');
|
|
const ServerlessError = require('./serverless-error');
|
|
const version = require('./../package.json').version;
|
|
const isStandaloneExecutable = require('./utils/is-standalone-executable');
|
|
const logDeprecation = require('./utils/log-deprecation');
|
|
const eventuallyUpdate = require('./utils/eventually-update');
|
|
const commmandsSchema = require('./cli/commands-schema');
|
|
const resolveCliInput = require('./cli/resolve-input');
|
|
const isDashboardEnabled = require('./configuration/is-dashboard-enabled');
|
|
const parseEntries = require('./configuration/variables/resolve-meta').parseEntries;
|
|
|
|
// Old local fallback is triggered in older versions by Serverless constructor directly
|
|
const isStackFromOldLocalFallback = RegExp.prototype.test.bind(
|
|
/lib[\\/]Serverless.js:\d+[^]+lib[\\/]Serverless.js:\d+/i
|
|
);
|
|
|
|
class Serverless {
|
|
constructor(config = {}) {
|
|
if (!isPlainObject(config)) config = {};
|
|
|
|
if (isStackFromOldLocalFallback(new Error().stack)) {
|
|
// Prevent old local fallback, as it's not compatible with this version of the Framework
|
|
const chalk = require('chalk');
|
|
throw new ServerlessError(
|
|
[
|
|
'Cannot run local installation of the Serverless Framework by the outdated global version.',
|
|
'Please upgrade via:',
|
|
'',
|
|
chalk.bold(
|
|
isStandaloneExecutable ? 'serverless upgrade --major' : 'npm install -g serverless'
|
|
),
|
|
chalk.grey(
|
|
'Note: Latest release can run any version of the locally installed Serverless Framework.'
|
|
),
|
|
'',
|
|
'Alternatively run locally installed version directly via:',
|
|
'',
|
|
chalk.bold('npx serverless <command> <options>'),
|
|
].join('\n'),
|
|
'UNSUPPORTED_LOCAL_FALLBACK'
|
|
);
|
|
}
|
|
|
|
this.serviceDir = ensureString(config.serviceDir, {
|
|
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(config.configurationFilename, {
|
|
name: 'config.configurationFilename',
|
|
Error: ServerlessError,
|
|
errorCode: 'INVALID_NON_STRING_CONFIGURATION_FILENAME',
|
|
});
|
|
if (path.isAbsolute(this.configurationFilename)) {
|
|
throw new ServerlessError(
|
|
`"config.configurationFilename" cannot be absolute path. Received: ${config.configurationFilename}`,
|
|
'INVALID_ABSOLUTE_PATH_CONFIGURATION_FILENAME'
|
|
);
|
|
}
|
|
this.configurationInput = ensurePlainObject(config.configuration, {
|
|
name: 'config.configuration',
|
|
Error: ServerlessError,
|
|
errorCode: 'INVALID_NON_OBJECT_CONFIGURATION',
|
|
});
|
|
}
|
|
|
|
const commands = ensureArray(config.commands);
|
|
// Ensure that original `options` are not mutated, can be removed after addressing:
|
|
// https://github.com/serverless/serverless/issues/2582
|
|
const cliOptions = { ...ensurePlainObject(config.options) };
|
|
this.processedInput = { commands, options: cliOptions };
|
|
|
|
this.providers = {};
|
|
|
|
this.version = version;
|
|
|
|
this.yamlParser = new YamlParser(this);
|
|
this.utils = new Utils(this);
|
|
this.service = new Service(this);
|
|
|
|
// Old variables resolver is dropped, yet some plugins access service properties through
|
|
// `variables` class. Below patch ensures those plugins won't get broken
|
|
this.variables = { service: this.service };
|
|
|
|
// `config.variablesMeta` will not be provided if the initial resolution of variables failed.
|
|
// We're ensuring it locally not to disrupt configuration extensions as eventually done by
|
|
// the plugins (which are still loaded in spite of the error, if e.g. help output was requested)
|
|
this.variablesMeta = config.variablesMeta || new Map([]);
|
|
this.pluginManager = new PluginManager(this);
|
|
this.configSchemaHandler = new ConfigSchemaHandler(this);
|
|
|
|
this.config = new Config(this, { serviceDir: config.serviceDir });
|
|
|
|
this.classes = {};
|
|
this.classes.CLI = CLI;
|
|
this.classes.YamlParser = YamlParser;
|
|
this.classes.Utils = Utils;
|
|
this.classes.Service = Service;
|
|
this.classes.Error = ServerlessError;
|
|
this.classes.PluginManager = PluginManager;
|
|
this.classes.ConfigSchemaHandler = ConfigSchemaHandler;
|
|
|
|
this.serverlessDirPath = path.join(os.homedir(), '.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;
|
|
}
|
|
|
|
async init() {
|
|
// create an instanceId (can be e.g. used when a predictable random value is needed)
|
|
this.instanceId = new Date().getTime().toString();
|
|
|
|
// create a new CLI instance
|
|
this.cli = new this.classes.CLI(this);
|
|
|
|
eventuallyUpdate(this);
|
|
// set the options and commands which were processed by the CLI
|
|
this.pluginManager.setCliOptions(this.processedInput.options);
|
|
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;
|
|
// give the CLI the plugins and commands so that it can print out
|
|
// information such as options when the user enters --help
|
|
this.cli.setLoadedPlugins(this.pluginManager.getPlugins());
|
|
this.cli.setLoadedCommands(this.pluginManager.getCommands());
|
|
}
|
|
async run() {
|
|
if (this.configurationInput) this.service.reloadServiceFileParam();
|
|
|
|
// make sure the command exists before doing anything else
|
|
this.pluginManager.validateCommand(this.processedInput.commands);
|
|
|
|
// Some plugins acccess `options` through `this.variables`
|
|
this.variables.options = this.pluginManager.cliOptions;
|
|
|
|
if (resolveCliInput().commands[0] !== 'plugin') {
|
|
// merge arrays after variables have been populated
|
|
// (https://github.com/serverless/serverless/issues/3511)
|
|
this.service.mergeArrays();
|
|
|
|
// populate function names after variables are loaded in case functions were externalized
|
|
// (https://github.com/serverless/serverless/issues/2997)
|
|
this.service.setFunctionNames(this.processedInput.options);
|
|
|
|
// If in context of service, validate the service configuration
|
|
if (this.serviceDir) await this.service.validate();
|
|
}
|
|
|
|
this.serviceOutputs = new Map();
|
|
this.servicePluginOutputs = new Map();
|
|
|
|
// trigger the plugin lifecycle when there's something which should be processed
|
|
await this.pluginManager.run(this.processedInput.commands);
|
|
}
|
|
|
|
addServiceOutputSection(sectionName, content) {
|
|
sectionName = ensureString(sectionName, { name: 'sectionName' });
|
|
if (typeof ensureValue(content, { name: 'content' }) !== 'string') {
|
|
content = ensureIterable(content, {
|
|
name: 'content',
|
|
denyEmpty: true,
|
|
ensureItem: ensureString,
|
|
});
|
|
} else if (!content) {
|
|
throw new TypeError('Section content cannot be empty string');
|
|
}
|
|
if (this.serviceOutputs.has(sectionName) || this.servicePluginOutputs.has(sectionName)) {
|
|
throw new TypeError(`Section content for "${sectionName}" was already set`);
|
|
}
|
|
this.servicePluginOutputs.set(sectionName, content);
|
|
}
|
|
|
|
setProvider(name, provider) {
|
|
this.providers[name] = provider;
|
|
}
|
|
|
|
getProvider(name) {
|
|
return this.providers[name] ? this.providers[name] : false;
|
|
}
|
|
|
|
getVersion() {
|
|
return this.version;
|
|
}
|
|
|
|
get isDashboardEnabled() {
|
|
return isDashboardEnabled({
|
|
configuration: this.configurationInput,
|
|
options: this.processedInput.options,
|
|
});
|
|
}
|
|
|
|
// Only for internal use
|
|
_logDeprecation(code, message) {
|
|
return logDeprecation(code, message, { serviceConfig: this.configurationInput });
|
|
}
|
|
|
|
// To be used by external plugins
|
|
logDeprecation(code, message) {
|
|
return this._logDeprecation(`EXT_${ensureString(code)}`, ensureString(message));
|
|
}
|
|
|
|
extendConfiguration(configurationPathKeys, value) {
|
|
configurationPathKeys = ensureArray(configurationPathKeys, {
|
|
ensureItem: ensureString,
|
|
});
|
|
if (configurationPathKeys.length < 1) {
|
|
throw new Error(
|
|
'Cannot extend configuration: ConfigurationPathKeys needs to contain at least one element.'
|
|
);
|
|
}
|
|
|
|
if (!this.isConfigurationExtendable) {
|
|
throw new Error(
|
|
'Cannot extend configuration: It can only be extended during initialization phase.'
|
|
);
|
|
}
|
|
try {
|
|
value = JSON.parse(JSON.stringify(value));
|
|
} catch (error) {
|
|
throw new Error(`Cannot extend configuration: Received non JSON value: ${value}`);
|
|
}
|
|
|
|
_.set(this.configurationInput, configurationPathKeys, value);
|
|
const metaPathPrefix = configurationPathKeys.join('\0');
|
|
for (const key of this.variablesMeta.keys()) {
|
|
if (key === metaPathPrefix || key.startsWith(`${metaPathPrefix}\0`)) {
|
|
this.variablesMeta.delete(key);
|
|
}
|
|
}
|
|
if (!_.isObject(value)) {
|
|
const lastKey = configurationPathKeys.pop();
|
|
value = { [lastKey]: value };
|
|
}
|
|
parseEntries(Object.entries(value), configurationPathKeys, this.variablesMeta);
|
|
}
|
|
}
|
|
|
|
module.exports = Serverless;
|