Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 43 additions & 15 deletions src/ComponentsService.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
'use strict';

const { resolve } = require('path');
const { isEmpty, path } = require('ramda');
const { isEmpty } = require('ramda');
const { Graph, alg } = require('@dagrejs/graphlib');
const traverse = require('traverse');
const pLimit = require('./utils/p-limit');
const ServerlessError = require('./serverless-error');
const {
createRegistry,
getOwnByPath,
hasOwn,
isReservedComponentId,
} = require('./utils/safe-object');
const utils = require('./utils');
const { loadComponent } = require('./load');
const colors = require('./cli/colors');
const ServerlessFramework = require('../components/framework');

const INTERNAL_COMPONENTS = {
const INTERNAL_COMPONENTS = Object.assign(createRegistry(), {
'serverless-framework': resolve(__dirname, '../components/framework'),
};
});

const formatError = (e) => {
let formattedError = e instanceof Error ? e.message : e;
Expand All @@ -34,7 +40,7 @@ const resolveObject = (object, context, method) => {
let newValue = value;
for (const match of matches) {
const referencedPropertyPath = match.substring(2, match.length - 1).split('.');
const referencedPropertyValue = path(referencedPropertyPath, context);
const referencedPropertyValue = getOwnByPath(context, referencedPropertyPath);

if (referencedPropertyValue === undefined) {
let errMsg = `The variable "${match}" cannot be resolved: the referenced output does not exist.`;
Expand Down Expand Up @@ -80,9 +86,16 @@ const validateGraph = (graph) => {
};

const getAllComponents = async (obj = {}, root = process.cwd()) => {
const allComponents = {};
const allComponents = createRegistry();

for (const [key, val] of Object.entries(obj.services)) {
if (isReservedComponentId(key)) {
throw new ServerlessError(
`Service alias "${key}" is reserved and cannot be used.`,
'INVALID_SERVICE_ALIAS'
);
}

// By default assume `serverless-framework` component
if (!val.component) {
val.component = 'serverless-framework';
Expand All @@ -101,7 +114,7 @@ const getAllComponents = async (obj = {}, root = process.cwd()) => {
path: localComponentPath,
inputs: val,
};
} else if (val.component in INTERNAL_COMPONENTS) {
} else if (hasOwn(INTERNAL_COMPONENTS, val.component)) {
// Internal component
allComponents[key] = {
path: INTERNAL_COMPONENTS[val.component],
Expand Down Expand Up @@ -157,7 +170,7 @@ const setDependencies = (allComponents) => {
for (const match of matches) {
const referencedComponent = match.substring(2, match.length - 1).split('.')[0];

if (!allComponents[referencedComponent]) {
if (!hasOwn(allComponents, referencedComponent)) {
throw new ServerlessError(
`The service "${referencedComponent}" does not exist. It is referenced by "${alias}" in expression "${match}".`,
'REFERENCED_COMPONENT_DOES_NOT_EXIST'
Expand All @@ -173,7 +186,7 @@ const setDependencies = (allComponents) => {

if (typeof allComponents[alias].inputs.dependsOn === 'string') {
const explicitDependency = allComponents[alias].inputs.dependsOn;
if (!allComponents[explicitDependency]) {
if (!hasOwn(allComponents, explicitDependency)) {
throw new ServerlessError(
`The service "${explicitDependency}" referenced in "dependsOn" of "${alias}" does not exist`,
'REFERENCED_COMPONENT_DOES_NOT_EXIST'
Expand All @@ -183,7 +196,7 @@ const setDependencies = (allComponents) => {
} else {
const explicitDependencies = allComponents[alias].inputs.dependsOn || [];
for (const explicitDependency of explicitDependencies) {
if (!allComponents[explicitDependency]) {
if (!hasOwn(allComponents, explicitDependency)) {
throw new ServerlessError(
`The service "${explicitDependency}" referenced in "dependsOn" of "${alias}" does not exist`,
'REFERENCED_COMPONENT_DOES_NOT_EXIST'
Expand Down Expand Up @@ -382,6 +395,13 @@ class ComponentsService {
}

async invokeComponentCommand(componentName, command, options) {
if (isReservedComponentId(componentName)) {
throw new ServerlessError(
`Service alias "${componentName}" is reserved and cannot be used.`,
'INVALID_SERVICE_ALIAS'
);
}

// We can have commands that do not have to call commands directly on the component,
// but are global commands that can accept the componentName parameter
// to filter out data
Expand All @@ -394,6 +414,7 @@ class ComponentsService {

const component =
this.allComponents &&
hasOwn(this.allComponents, componentName) &&
this.allComponents[componentName] &&
this.allComponents[componentName].instance;
if (component === undefined) {
Expand All @@ -402,32 +423,39 @@ class ComponentsService {
this.context.logVerbose(`Invoking "${command}" on service "${componentName}"`);

const isDefaultCommand = ['deploy', 'remove', 'logs', 'info', 'package'].includes(command);
const hasCustomCommand =
component && component.commands && hasOwn(component.commands, command);

if (isDefaultCommand) {
// Default command defined for all components (deploy, logs, dev, etc.)
if (!component || !component[command]) {
if (!component || typeof component[command] !== 'function') {
throw new ServerlessError(
`No method "${command}" on service "${componentName}"`,
'COMPONENT_COMMAND_NOT_FOUND'
);
}
handler = (opts) => component[command](opts);
} else if (
(!component || !component.commands || !component.commands[command]) &&
component instanceof ServerlessFramework
) {
} else if (!hasCustomCommand && component instanceof ServerlessFramework) {
// Workaround to invoke all custom Framework commands
// TODO: Support options and validation
handler = (opts) => component.command(command, opts);
} else {
// Custom command: the handler is defined in the component's `commands` property
if (!component || !component.commands || !component.commands[command]) {
if (!hasCustomCommand) {
throw new ServerlessError(
`No command "${command}" on service ${componentName}`,
'COMPONENT_COMMAND_NOT_FOUND'
);
}
const commandHandler = component.commands[command].handler;

if (typeof commandHandler !== 'function') {
throw new ServerlessError(
`No command "${command}" on service ${componentName}`,
'COMPONENT_COMMAND_NOT_FOUND'
);
}

handler = (opts) => commandHandler.call(component, opts);
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Context.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const formatOutput = require('./cli/format-output');
const Progresses = require('./cli/Progresses');
const { stderrCliColors } = require('./cli/colors');
const ServerlessError = require('./serverless-error');
const { createRegistry } = require('./utils/safe-object');

class Context {
constructor(config) {
Expand All @@ -21,7 +22,7 @@ class Context {
/** @type {string} */
this.stage = config.stage;
this.id = undefined;
this.componentCommandsOutcomes = {};
this.componentCommandsOutcomes = createRegistry();
this.hasEnabledVerboseInteractively = false;

this.progresses = new Progresses(this.output);
Expand Down
19 changes: 10 additions & 9 deletions src/cli/Progresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { stderrCliColors: colors } = require('./colors');
const symbols = require('./symbols');
const isUnicodeSupported = require('is-unicode-supported');
const { stripVTControlCharacters: stripAnsi } = require('node:util');
const { createRegistry, hasOwn } = require('../utils/safe-object');

const dots = {
frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
Expand Down Expand Up @@ -40,7 +41,7 @@ class Progresses {
spinner: isUnicodeSupported() ? dots : dashes,
};
/** @type {Record<string, Progress>} */
this.progresses = {};
this.progresses = createRegistry();
this.isCursorHidden = false;
this.currentInterval = null;
this.output = output;
Expand Down Expand Up @@ -86,22 +87,22 @@ class Progresses {
* @param {string} name
*/
exists(name) {
return this.progresses[name];
return hasOwn(this.progresses, name) ? this.progresses[name] : undefined;
}

/**
* @param {string} name
*/
isWaiting(name) {
return this.progresses[name] && this.progresses[name].status === 'waiting';
return hasOwn(this.progresses, name) && this.progresses[name].status === 'waiting';
}

/**
* @param {string} name
* @param {string} text
*/
update(name, text) {
if (!this.progresses[name]) throw Error(`No progress with name ${name}`);
if (!hasOwn(this.progresses, name)) throw Error(`No progress with name ${name}`);
this.progresses[name].text = text;
this.updateSpinnerState(name);
}
Expand All @@ -111,7 +112,7 @@ class Progresses {
* @param {string} [text]
*/
success(name, text) {
if (!this.progresses[name]) throw Error(`No progress with name ${name}`);
if (!hasOwn(this.progresses, name)) throw Error(`No progress with name ${name}`);
this.progresses[name].status = 'success';
if (text) {
this.progresses[name].text = text;
Expand All @@ -127,7 +128,7 @@ class Progresses {
error(name, error) {
const errorMessage = error instanceof Error ? error.message : error;

if (!this.progresses[name]) throw Error(`No progress with name ${name}`);
if (!hasOwn(this.progresses, name)) throw Error(`No progress with name ${name}`);
this.progresses[name].status = 'error';
this.progresses[name].text = 'error';
this.progresses[name].endTime = Date.now();
Expand All @@ -143,7 +144,7 @@ class Progresses {
* @param {string} name
*/
skipped(name) {
if (!this.progresses[name]) throw Error(`No progress with name ${name}`);
if (!hasOwn(this.progresses, name)) throw Error(`No progress with name ${name}`);
this.progresses[name].status = 'skipped';
this.progresses[name].text = 'skipped';
this.progresses[name].endTime = Date.now();
Expand All @@ -167,12 +168,12 @@ class Progresses {
this.isCursorHidden = false;
cliCursor.show();
}
this.progresses = {};
this.progresses = createRegistry();
}

updateSpinnerState(progressName) {
// Log the contents of the progress that initiated state update to verbose
if (this.progresses[progressName]) {
if (hasOwn(this.progresses, progressName)) {
const progress = this.progresses[progressName];
let logMessage = progress.text;
if (progress.content) {
Expand Down
14 changes: 11 additions & 3 deletions src/configuration/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ const path = require('path');
const ServerlessError = require('../serverless-error');
const isObject = require('type/object/is');
const { default: Ajv } = require('ajv');
const { RESERVED_COMPONENT_IDS, hasOwn, isReservedComponentId } = require('../utils/safe-object');

function validateConfiguration(configuration, configurationPath) {
const configurationFilename = path.basename(configurationPath);
if (typeof configuration !== 'object') {
if (configuration == null || typeof configuration !== 'object') {
throw new ServerlessError(
`Resolved "${configurationFilename}" does not contain valid Compose configuration.\n` +
'Read about Serverless Framework Compose in the documentation: https://slss.io/docs-compose',
'INVALID_NON_OBJECT_CONFIGURATION'
);
}

if (!isObject(configuration.services)) {
if (!hasOwn(configuration, 'services') || !isObject(configuration.services)) {
throw new ServerlessError(
`Invalid configuration: "${configurationFilename}" must contain "services" property.\n` +
Comment thread
GrahamCampbell marked this conversation as resolved.
'Read about Serverless Framework Compose configuration in the documentation: https://slss.io/docs-compose',
Expand All @@ -24,6 +25,13 @@ function validateConfiguration(configuration, configurationPath) {
}

Object.entries(configuration.services).forEach(([key, value]) => {
if (isReservedComponentId(key)) {
throw new ServerlessError(
`Invalid configuration: definition of "${key}" service uses a reserved alias. ` +
`Reserved aliases: ${Array.from(RESERVED_COMPONENT_IDS).join(', ')}.`,
'INVALID_SERVICE_ALIAS'
);
}
if (!isObject(value)) {
throw new ServerlessError(
`Invalid configuration: definition of "${key}" service must be an object.\n` +
Expand All @@ -47,7 +55,7 @@ function validateConfiguration(configuration, configurationPath) {
'custom',
];
frameworkConfigKeys.forEach((key) => {
if (key in configuration) {
if (hasOwn(configuration, key)) {
throw new ServerlessError(
`Invalid property "${key}" in "${configurationFilename}".\n` +
'This is a Serverless Framework option (serverless.yml) that is not supported in serverless-compose.yml.\n' +
Expand Down
Loading