Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of serverless.js configuration file #4590

Merged
merged 5 commits into from Dec 22, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/providers/aws/guide/intro.md
Expand Up @@ -58,7 +58,7 @@ The Serverless Framework not only deploys your Functions and the Events that tri

### Services

A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this:
A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this:

```yml
# serverless.yml
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/azure/guide/intro.md
Expand Up @@ -64,7 +64,7 @@ A **Service** is the Framework's unit of organization. You can think of it as a
project file, though you can have multiple services for a single application.
It's where you define your Functions, the Events that trigger them, and the
Resources your Functions use, all in one file entitled `serverless.yml` (or
`serverless.json`). It looks like this:
`serverless.json` or `serverless.js`). It looks like this:

```yml
# serverless.yml
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/google/guide/intro.md
Expand Up @@ -46,7 +46,7 @@ When you define an event for your Google Cloud Function in the Serverless Framew

### Services

A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this:
A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this:

```yml
# serverless.yml
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/kubeless/guide/intro.md
Expand Up @@ -42,7 +42,7 @@ Anything that triggers an Kubeless Event to execute is regarded by the Framework

### Services

A **Service** is the Serverless Framework's unit of organization (not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/). You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the Events that trigger them, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this:
A **Service** is the Serverless Framework's unit of organization (not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/). You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the Events that trigger them, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this:

```yml
# serverless.yml
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/openwhisk/guide/intro.md
Expand Up @@ -47,7 +47,7 @@ When you define an event for your Apache OpenWhisk Action in the Serverless Fram

### Services

A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this:
A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this:

```yml
# serverless.yml
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/spotinst/guide/intro.md
Expand Up @@ -73,7 +73,7 @@ module.exports.main = function main (event, context, callback) {

### Services

A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this:
A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this:

```yml

Expand Down
2 changes: 1 addition & 1 deletion docs/providers/webtasks/guide/intro.md
Expand Up @@ -24,7 +24,7 @@ Here are the Framework's main concepts and how they pertain to Auth0 Webtasks...

A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application.

The Auth0 Webtasks platform was designed to be simple and easy to use with minimal configuration. Therefore, services that uses Auth0 Webtasks are just a few lines of configuration in a single file, entitled `serverless.yml` (or `serverless.json`). It looks like this:
The Auth0 Webtasks platform was designed to be simple and easy to use with minimal configuration. Therefore, services that uses Auth0 Webtasks are just a few lines of configuration in a single file, entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this:

```yml
# serverless.yml
Expand Down
121 changes: 68 additions & 53 deletions lib/classes/Service.js
Expand Up @@ -46,6 +46,7 @@ class Service {
'serverless.yaml',
'serverless.yml',
'serverless.json',
'serverless.js',
];

const serviceFilePaths = _.map(serviceFilenames, filename => path.join(servicePath, filename));
Expand All @@ -61,65 +62,79 @@ class Service {
serviceFilenames[serviceFileIndex] :
_.first(serviceFilenames);

if (serviceFilename === 'serverless.js') {
return new BbPromise((resolve) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use the new Promise anti-pattern! Additionally require can throw synchronously, so it has to be catched.

return BbPromise.try(() => that.loadServiceFileParam(serviceFilename, require(serviceFilePath)));

// use require to load serverless.js file
// eslint-disable-next-line global-require
resolve(that.loadServiceFileParam(serviceFilename, require(serviceFilePath)));
});
}

return that.serverless.yamlParser
.parse(serviceFilePath)
.then((serverlessFileParam) => {
const serverlessFile = serverlessFileParam;
// basic service level validation
const version = this.serverless.utils.getVersion();
const ymlVersion = serverlessFile.frameworkVersion;
if (ymlVersion && !semver.satisfies(version, ymlVersion)) {
const errorMessage = [
`The Serverless version (${version}) does not satisfy the`,
` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`,
].join('');
throw new ServerlessError(errorMessage);
}
if (!serverlessFile.service) {
throw new ServerlessError(`"service" property is missing in ${serviceFilename}`);
}
if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) {
throw new ServerlessError(`"service" is missing the "name" property in ${serviceFilename}`); // eslint-disable-line max-len
}
if (!serverlessFile.provider) {
throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`);
}
.then((serverlessFileParam) =>
that.loadServiceFileParam(serviceFilename, serverlessFileParam)
);
}

if (typeof serverlessFile.provider !== 'object') {
const providerName = serverlessFile.provider;
serverlessFile.provider = {
name: providerName,
};
}
loadServiceFileParam(serviceFilename, serverlessFileParam) {
const that = this;

if (_.isObject(serverlessFile.service)) {
that.serviceObject = serverlessFile.service;
that.service = serverlessFile.service.name;
} else {
that.serviceObject = { name: serverlessFile.service };
that.service = serverlessFile.service;
}
const serverlessFile = serverlessFileParam;
// basic service level validation
const version = this.serverless.utils.getVersion();
const ymlVersion = serverlessFile.frameworkVersion;
if (ymlVersion && !semver.satisfies(version, ymlVersion)) {
const errorMessage = [
`The Serverless version (${version}) does not satisfy the`,
` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`,
].join('');
throw new ServerlessError(errorMessage);
}
if (!serverlessFile.service) {
throw new ServerlessError(`"service" property is missing in ${serviceFilename}`);
}
if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) {
throw new ServerlessError(`"service" is missing the "name" property in ${serviceFilename}`); // eslint-disable-line max-len
}
if (!serverlessFile.provider) {
throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`);
}

that.custom = serverlessFile.custom;
that.plugins = serverlessFile.plugins;
that.resources = serverlessFile.resources;
that.functions = serverlessFile.functions || {};

// merge so that the default settings are still in place and
// won't be overwritten
that.provider = _.merge(that.provider, serverlessFile.provider);

if (serverlessFile.package) {
that.package.individually = serverlessFile.package.individually;
that.package.path = serverlessFile.package.path;
that.package.artifact = serverlessFile.package.artifact;
that.package.exclude = serverlessFile.package.exclude;
that.package.include = serverlessFile.package.include;
that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies;
}
if (typeof serverlessFile.provider !== 'object') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consistently use lodash:

if (!_.isObject(serverlessFile.provider)) ...

const providerName = serverlessFile.provider;
serverlessFile.provider = {
name: providerName,
};
}

return this;
});
if (_.isObject(serverlessFile.service)) {
that.serviceObject = serverlessFile.service;
that.service = serverlessFile.service.name;
} else {
that.serviceObject = { name: serverlessFile.service };
that.service = serverlessFile.service;
}

that.custom = serverlessFile.custom;
that.plugins = serverlessFile.plugins;
that.resources = serverlessFile.resources;
that.functions = serverlessFile.functions || {};

// merge so that the default settings are still in place and
// won't be overwritten
that.provider = _.merge(that.provider, serverlessFile.provider);

if (serverlessFile.package) {
that.package.individually = serverlessFile.package.individually;
that.package.path = serverlessFile.package.path;
that.package.artifact = serverlessFile.package.artifact;
that.package.exclude = serverlessFile.package.exclude;
that.package.include = serverlessFile.package.include;
that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies;
}

return this;
}

setFunctionNames(rawOptions) {
Expand Down
55 changes: 55 additions & 0 deletions lib/classes/Service.test.js
Expand Up @@ -287,6 +287,61 @@ describe('Service', () => {
});
});

it('should load serverless.js from filesystem', () => {
const SUtils = new Utils();
const serverlessJSON = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}',
},
plugins: ['testPlugin'],
functions: {
functionA: {},
},
resources: {
aws: {
resourcesProp: 'value',
},
azure: {},
google: {},
},
package: {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
},
};

SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.js'),
`module.exports = ${JSON.stringify(serverlessJSON)};`);

const serverless = new Serverless();
serverless.config.update({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);

return expect(serviceInstance.load()).to.eventually.be.fulfilled
.then(() => {
expect(serviceInstance.service).to.be.equal('new-service');
expect(serviceInstance.provider.name).to.deep.equal('aws');
expect(serviceInstance.provider.variableSyntax).to.equal(
'\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}'
);
expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.azure).to.deep.equal({});
expect(serviceInstance.resources.google).to.deep.equal({});
expect(serviceInstance.package.exclude.length).to.equal(1);
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
});
});

it('should load YAML in favor of JSON', () => {
const SUtils = new Utils();
const serverlessJSON = {
Expand Down
2 changes: 2 additions & 0 deletions lib/classes/Utils.js
Expand Up @@ -105,6 +105,8 @@ class Utils {
servicePath = process.cwd();
} else if (fileExistsSync(path.join(process.cwd(), 'serverless.json'))) {
servicePath = process.cwd();
} else if (fileExistsSync(path.join(process.cwd(), 'serverless.js'))) {
servicePath = process.cwd();
}

return servicePath;
Expand Down
12 changes: 12 additions & 0 deletions lib/classes/Utils.test.js
Expand Up @@ -281,6 +281,18 @@ describe('Utils', () => {
expect(servicePath).to.not.equal(null);
});

it('should detect if the CWD is a service directory when using Serverless .js files', () => {
const tmpDirPath = testUtils.getTmpDirPath();
const tmpFilePath = path.join(tmpDirPath, 'serverless.js');

serverless.utils.writeFileSync(tmpFilePath, 'foo');
process.chdir(tmpDirPath);

const servicePath = serverless.utils.findServicePath();

expect(servicePath).to.not.equal(null);
});

it('should detect if the CWD is not a service directory', () => {
// just use the root of the tmpdir because findServicePath will
// also check parent directories (and may find matching tmp dirs
Expand Down
1 change: 1 addition & 0 deletions lib/plugins/package/lib/packageService.js
Expand Up @@ -14,6 +14,7 @@ module.exports = {
'serverless.yml',
'serverless.yaml',
'serverless.json',
'serverless.js',
'.serverless/**',
'.serverless_plugins/**',
],
Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/package/lib/packageService.test.js
Expand Up @@ -82,7 +82,7 @@ describe('#packageService()', () => {
expect(exclude).to.deep.equal([
'.git/**', '.gitignore', '.DS_Store',
'npm-debug.log', 'serverless.yml',
'serverless.yaml', 'serverless.json',
'serverless.yaml', 'serverless.json', 'serverless.js',
'.serverless/**', '.serverless_plugins/**',
'dir', 'file.js',
]);
Expand All @@ -102,7 +102,7 @@ describe('#packageService()', () => {
expect(exclude).to.deep.equal([
'.git/**', '.gitignore', '.DS_Store',
'npm-debug.log', 'serverless.yml',
'serverless.yaml', 'serverless.json',
'serverless.yaml', 'serverless.json', 'serverless.js',
'.serverless/**', '.serverless_plugins/**',
'dir', 'file.js', 'lib', 'other.js',
]);
Expand Down
8 changes: 8 additions & 0 deletions lib/plugins/plugin/install/install.js
Expand Up @@ -109,6 +109,14 @@ class PluginInstall {

addPluginToServerlessFile() {
return this.getServerlessFilePath().then(serverlessFilePath => {
if (_.last(_.split(serverlessFilePath, '.')) === 'js') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need the same process in plugin/uninstall/uninstall.js as well. Otherwise, a plugin can't be removed from serverless.js when running sls plugin uninstall

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, added log message for uninstall as well 👍

this.serverless.cli.log(`
Can't automatically add plugin into "serverless.js" file.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why cannot a plugin be installed automatically?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the js file only exports the serverless object, but hides the details how that is kept or generated. So the plugin plugin does not have any chance to tell the serverless.js to include the new plugin into the exported object the next time it is loaded.
In theory the whole service definition could be dynamically compiled depending on some environment settings. That way we have to treat the serverless.js as opaque and the exposed object as read-only.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I see 😄

Please make it manually.
`);
return new BbPromise((resolve) => resolve());
}

if (_.last(_.split(serverlessFilePath, '.')) === 'json') {
return fse.readJsonAsync(serverlessFilePath).then(serverlessFileObj => {
const newServerlessFileObj = serverlessFileObj;
Expand Down
18 changes: 18 additions & 0 deletions lib/plugins/plugin/install/install.test.js
Expand Up @@ -357,6 +357,24 @@ describe('PluginInstall', () => {
});
});
});

it('should not modify serverless .js file', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here.

const serverlessJsFilePath = path.join(servicePath, 'serverless.js');
const serverlessJson = {
service: 'plugin-service',
provider: 'aws',
plugins: [],
};
serverless.utils
.writeFileSync(serverlessJsFilePath, `module.exports = ${JSON.stringify(serverlessJson)};`);
pluginInstall.options.pluginName = 'serverless-plugin-1';
return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
// use require to load serverless.js
// eslint-disable-next-line global-require
expect(require(serverlessJsFilePath).plugins)
.to.be.deep.equal([]);
});
});
});

describe('#installPeerDependencies()', () => {
Expand Down
8 changes: 7 additions & 1 deletion lib/plugins/plugin/lib/utils.js
Expand Up @@ -22,14 +22,20 @@ module.exports = {
const serverlessYmlFilePath = path.join(servicePath, 'serverless.yml');
const serverlessYamlFilePath = path.join(servicePath, 'serverless.yaml');
const serverlessJsonFilePath = path.join(servicePath, 'serverless.json');
const serverlessJsFilePath = path.join(servicePath, 'serverless.js');

return fileExists(serverlessYmlFilePath)
.then(ymlExists => {
if (!ymlExists) {
return fileExists(serverlessYamlFilePath)
.then(yamlExists => {
if (!yamlExists) {
return serverlessJsonFilePath;
return fileExists(serverlessJsonFilePath).then((jsonExists) => {
if (jsonExists) {
return serverlessJsonFilePath;
}
return serverlessJsFilePath;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we check here for the existence of the JS file too and reject with an error as last resort?

return fileExists(serverlessJsFilePath).then(jsExists => {
  if (jsExists) {
    return serverlessJsFilePath;
  }
  return BbPromise.reject(new this.serverless.classes.Error('Could not find any serverless service definition file.'));
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored this method a bit, looks much nicer now.

});
}
return serverlessYamlFilePath;
});
Expand Down
10 changes: 10 additions & 0 deletions lib/plugins/plugin/lib/utils.test.js
Expand Up @@ -99,6 +99,16 @@ describe('PluginUtils', () => {
expect(serverlessFilePath).to.equal(serverlessJsonFilePath);
});
});

it('should return the correct serverless file path for a .js file', () => {
const serverlessJsFilePath = path.join(servicePath, 'serverless.js');
fse.ensureFileSync(serverlessJsFilePath);

return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled
.then(serverlessFilePath => {
expect(serverlessFilePath).to.equal(serverlessJsFilePath);
});
});
});

describe('#getPlugins()', () => {
Expand Down