Skip to content

Commit bca4da5

Browse files
committed
add support for serverless.js
1 parent 95f6789 commit bca4da5

File tree

19 files changed

+210
-69
lines changed

19 files changed

+210
-69
lines changed

docs/providers/aws/guide/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ The Serverless Framework not only deploys your Functions and the Events that tri
5858

5959
### Services
6060

61-
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:
61+
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:
6262

6363
```yml
6464
# serverless.yml

docs/providers/azure/guide/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ A **Service** is the Framework's unit of organization. You can think of it as a
6464
project file, though you can have multiple services for a single application.
6565
It's where you define your Functions, the Events that trigger them, and the
6666
Resources your Functions use, all in one file entitled `serverless.yml` (or
67-
`serverless.json`). It looks like this:
67+
`serverless.json` or `serverless.js`). It looks like this:
6868

6969
```yml
7070
# serverless.yml

docs/providers/google/guide/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ When you define an event for your Google Cloud Function in the Serverless Framew
4646

4747
### Services
4848

49-
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:
49+
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:
5050

5151
```yml
5252
# serverless.yml

docs/providers/kubeless/guide/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Anything that triggers an Kubeless Event to execute is regarded by the Framework
4242

4343
### Services
4444

45-
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:
45+
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:
4646

4747
```yml
4848
# serverless.yml

docs/providers/openwhisk/guide/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ When you define an event for your Apache OpenWhisk Action in the Serverless Fram
4747

4848
### Services
4949

50-
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:
50+
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:
5151

5252
```yml
5353
# serverless.yml

docs/providers/spotinst/guide/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ module.exports.main = function main (event, context, callback) {
7373

7474
### Services
7575

76-
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:
76+
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:
7777

7878
```yml
7979

docs/providers/webtasks/guide/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Here are the Framework's main concepts and how they pertain to Auth0 Webtasks...
2424

2525
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.
2626

27-
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:
27+
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:
2828

2929
```yml
3030
# serverless.yml

lib/classes/Service.js

Lines changed: 68 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Service {
4646
'serverless.yaml',
4747
'serverless.yml',
4848
'serverless.json',
49+
'serverless.js',
4950
];
5051

5152
const serviceFilePaths = _.map(serviceFilenames, filename => path.join(servicePath, filename));
@@ -61,65 +62,77 @@ class Service {
6162
serviceFilenames[serviceFileIndex] :
6263
_.first(serviceFilenames);
6364

64-
return that.serverless.yamlParser
65-
.parse(serviceFilePath)
66-
.then((serverlessFileParam) => {
67-
const serverlessFile = serverlessFileParam;
68-
// basic service level validation
69-
const version = this.serverless.utils.getVersion();
70-
const ymlVersion = serverlessFile.frameworkVersion;
71-
if (ymlVersion && !semver.satisfies(version, ymlVersion)) {
72-
const errorMessage = [
73-
`The Serverless version (${version}) does not satisfy the`,
74-
` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`,
75-
].join('');
76-
throw new ServerlessError(errorMessage);
77-
}
78-
if (!serverlessFile.service) {
79-
throw new ServerlessError(`"service" property is missing in ${serviceFilename}`);
80-
}
81-
if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) {
82-
throw new ServerlessError(`"service" is missing the "name" property in ${serviceFilename}`); // eslint-disable-line max-len
83-
}
84-
if (!serverlessFile.provider) {
85-
throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`);
86-
}
65+
if (serviceFilename === 'serverless.js') {
66+
return new BbPromise((resolve) => {
67+
resolve(that.loadServiceFileParam(serviceFilename, require(serviceFilePath)));
68+
});
69+
} else {
70+
return that.serverless.yamlParser
71+
.parse(serviceFilePath)
72+
.then((serverlessFileParam) => {
73+
return that.loadServiceFileParam(serviceFilename, serverlessFileParam);
74+
});
75+
}
76+
}
8777

88-
if (typeof serverlessFile.provider !== 'object') {
89-
const providerName = serverlessFile.provider;
90-
serverlessFile.provider = {
91-
name: providerName,
92-
};
93-
}
78+
loadServiceFileParam(serviceFilename, serverlessFileParam) {
79+
const that = this;
9480

95-
if (_.isObject(serverlessFile.service)) {
96-
that.serviceObject = serverlessFile.service;
97-
that.service = serverlessFile.service.name;
98-
} else {
99-
that.serviceObject = { name: serverlessFile.service };
100-
that.service = serverlessFile.service;
101-
}
81+
const serverlessFile = serverlessFileParam;
82+
// basic service level validation
83+
const version = this.serverless.utils.getVersion();
84+
const ymlVersion = serverlessFile.frameworkVersion;
85+
if (ymlVersion && !semver.satisfies(version, ymlVersion)) {
86+
const errorMessage = [
87+
`The Serverless version (${version}) does not satisfy the`,
88+
` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`,
89+
].join('');
90+
throw new ServerlessError(errorMessage);
91+
}
92+
if (!serverlessFile.service) {
93+
throw new ServerlessError(`"service" property is missing in ${serviceFilename}`);
94+
}
95+
if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) {
96+
throw new ServerlessError(`"service" is missing the "name" property in ${serviceFilename}`); // eslint-disable-line max-len
97+
}
98+
if (!serverlessFile.provider) {
99+
throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`);
100+
}
102101

103-
that.custom = serverlessFile.custom;
104-
that.plugins = serverlessFile.plugins;
105-
that.resources = serverlessFile.resources;
106-
that.functions = serverlessFile.functions || {};
107-
108-
// merge so that the default settings are still in place and
109-
// won't be overwritten
110-
that.provider = _.merge(that.provider, serverlessFile.provider);
111-
112-
if (serverlessFile.package) {
113-
that.package.individually = serverlessFile.package.individually;
114-
that.package.path = serverlessFile.package.path;
115-
that.package.artifact = serverlessFile.package.artifact;
116-
that.package.exclude = serverlessFile.package.exclude;
117-
that.package.include = serverlessFile.package.include;
118-
that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies;
119-
}
102+
if (typeof serverlessFile.provider !== 'object') {
103+
const providerName = serverlessFile.provider;
104+
serverlessFile.provider = {
105+
name: providerName,
106+
};
107+
}
120108

121-
return this;
122-
});
109+
if (_.isObject(serverlessFile.service)) {
110+
that.serviceObject = serverlessFile.service;
111+
that.service = serverlessFile.service.name;
112+
} else {
113+
that.serviceObject = { name: serverlessFile.service };
114+
that.service = serverlessFile.service;
115+
}
116+
117+
that.custom = serverlessFile.custom;
118+
that.plugins = serverlessFile.plugins;
119+
that.resources = serverlessFile.resources;
120+
that.functions = serverlessFile.functions || {};
121+
122+
// merge so that the default settings are still in place and
123+
// won't be overwritten
124+
that.provider = _.merge(that.provider, serverlessFile.provider);
125+
126+
if (serverlessFile.package) {
127+
that.package.individually = serverlessFile.package.individually;
128+
that.package.path = serverlessFile.package.path;
129+
that.package.artifact = serverlessFile.package.artifact;
130+
that.package.exclude = serverlessFile.package.exclude;
131+
that.package.include = serverlessFile.package.include;
132+
that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies;
133+
}
134+
135+
return this;
123136
}
124137

125138
setFunctionNames(rawOptions) {

lib/classes/Service.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,61 @@ describe('Service', () => {
287287
});
288288
});
289289

290+
it('should load serverless.js from filesystem', () => {
291+
const SUtils = new Utils();
292+
const serverlessJSON = {
293+
service: 'new-service',
294+
provider: {
295+
name: 'aws',
296+
stage: 'dev',
297+
region: 'us-east-1',
298+
variableSyntax: '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}',
299+
},
300+
plugins: ['testPlugin'],
301+
functions: {
302+
functionA: {},
303+
},
304+
resources: {
305+
aws: {
306+
resourcesProp: 'value',
307+
},
308+
azure: {},
309+
google: {},
310+
},
311+
package: {
312+
exclude: ['exclude-me'],
313+
include: ['include-me'],
314+
artifact: 'some/path/foo.zip',
315+
},
316+
};
317+
318+
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.js'),
319+
`module.exports = ${JSON.stringify(serverlessJSON)};`);
320+
321+
const serverless = new Serverless();
322+
serverless.config.update({ servicePath: tmpDirPath });
323+
serviceInstance = new Service(serverless);
324+
325+
return expect(serviceInstance.load()).to.eventually.be.fulfilled
326+
.then(() => {
327+
expect(serviceInstance.service).to.be.equal('new-service');
328+
expect(serviceInstance.provider.name).to.deep.equal('aws');
329+
expect(serviceInstance.provider.variableSyntax).to.equal(
330+
'\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}'
331+
);
332+
expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
333+
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
334+
expect(serviceInstance.resources.azure).to.deep.equal({});
335+
expect(serviceInstance.resources.google).to.deep.equal({});
336+
expect(serviceInstance.package.exclude.length).to.equal(1);
337+
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
338+
expect(serviceInstance.package.include.length).to.equal(1);
339+
expect(serviceInstance.package.include[0]).to.equal('include-me');
340+
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
341+
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
342+
});
343+
});
344+
290345
it('should load YAML in favor of JSON', () => {
291346
const SUtils = new Utils();
292347
const serverlessJSON = {

lib/classes/Utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ class Utils {
105105
servicePath = process.cwd();
106106
} else if (fileExistsSync(path.join(process.cwd(), 'serverless.json'))) {
107107
servicePath = process.cwd();
108+
} else if (fileExistsSync(path.join(process.cwd(), 'serverless.js'))) {
109+
servicePath = process.cwd();
108110
}
109111

110112
return servicePath;

lib/classes/Utils.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,18 @@ describe('Utils', () => {
281281
expect(servicePath).to.not.equal(null);
282282
});
283283

284+
it('should detect if the CWD is a service directory when using Serverless .js files', () => {
285+
const tmpDirPath = testUtils.getTmpDirPath();
286+
const tmpFilePath = path.join(tmpDirPath, 'serverless.js');
287+
288+
serverless.utils.writeFileSync(tmpFilePath, 'foo');
289+
process.chdir(tmpDirPath);
290+
291+
const servicePath = serverless.utils.findServicePath();
292+
293+
expect(servicePath).to.not.equal(null);
294+
});
295+
284296
it('should detect if the CWD is not a service directory', () => {
285297
// just use the root of the tmpdir because findServicePath will
286298
// also check parent directories (and may find matching tmp dirs

lib/plugins/package/lib/packageService.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module.exports = {
1414
'serverless.yml',
1515
'serverless.yaml',
1616
'serverless.json',
17+
'serverless.js',
1718
'.serverless/**',
1819
'.serverless_plugins/**',
1920
],

lib/plugins/package/lib/packageService.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('#packageService()', () => {
8282
expect(exclude).to.deep.equal([
8383
'.git/**', '.gitignore', '.DS_Store',
8484
'npm-debug.log', 'serverless.yml',
85-
'serverless.yaml', 'serverless.json',
85+
'serverless.yaml', 'serverless.json', 'serverless.js',
8686
'.serverless/**', '.serverless_plugins/**',
8787
'dir', 'file.js',
8888
]);
@@ -102,7 +102,7 @@ describe('#packageService()', () => {
102102
expect(exclude).to.deep.equal([
103103
'.git/**', '.gitignore', '.DS_Store',
104104
'npm-debug.log', 'serverless.yml',
105-
'serverless.yaml', 'serverless.json',
105+
'serverless.yaml', 'serverless.json', 'serverless.js',
106106
'.serverless/**', '.serverless_plugins/**',
107107
'dir', 'file.js', 'lib', 'other.js',
108108
]);

lib/plugins/plugin/install/install.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ class PluginInstall {
109109

110110
addPluginToServerlessFile() {
111111
return this.getServerlessFilePath().then(serverlessFilePath => {
112+
if (_.last(_.split(serverlessFilePath, '.')) === 'js') {
113+
this.serverless.cli.log(`
114+
Can't automatically add plugin into "serverless.js" file.
115+
Please make it manually.
116+
`);
117+
return new BbPromise((resolve) => resolve());
118+
}
119+
112120
if (_.last(_.split(serverlessFilePath, '.')) === 'json') {
113121
return fse.readJsonAsync(serverlessFilePath).then(serverlessFileObj => {
114122
const newServerlessFileObj = serverlessFileObj;

lib/plugins/plugin/install/install.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,21 @@ describe('PluginInstall', () => {
357357
});
358358
});
359359
});
360+
361+
it('should not modify serverless .js file', () => {
362+
const serverlessJsFilePath = path.join(servicePath, 'serverless.js');
363+
const serverlessJson = {
364+
service: 'plugin-service',
365+
provider: 'aws',
366+
};
367+
serverless.utils
368+
.writeFileSync(serverlessJsFilePath, serverlessJson);
369+
pluginInstall.options.pluginName = 'serverless-plugin-1';
370+
return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
371+
expect(serverless.utils.readFileSync(serverlessJsFilePath, 'utf8').plugins)
372+
.to.be.undefined;
373+
})
374+
});
360375
});
361376

362377
describe('#installPeerDependencies()', () => {

lib/plugins/plugin/lib/utils.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,21 @@ module.exports = {
2222
const serverlessYmlFilePath = path.join(servicePath, 'serverless.yml');
2323
const serverlessYamlFilePath = path.join(servicePath, 'serverless.yaml');
2424
const serverlessJsonFilePath = path.join(servicePath, 'serverless.json');
25+
const serverlessJsFilePath = path.join(servicePath, 'serverless.js');
2526

2627
return fileExists(serverlessYmlFilePath)
2728
.then(ymlExists => {
2829
if (!ymlExists) {
2930
return fileExists(serverlessYamlFilePath)
3031
.then(yamlExists => {
3132
if (!yamlExists) {
32-
return serverlessJsonFilePath;
33+
return fileExists(serverlessJsonFilePath).then((jsonExists) => {
34+
if (jsonExists) {
35+
return serverlessJsonFilePath;
36+
} else {
37+
return serverlessJsFilePath;
38+
}
39+
});
3340
}
3441
return serverlessYamlFilePath;
3542
});

lib/plugins/plugin/lib/utils.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ describe('PluginUtils', () => {
9999
expect(serverlessFilePath).to.equal(serverlessJsonFilePath);
100100
});
101101
});
102+
103+
it('should return the correct serverless file path for a .js file', () => {
104+
const serverlessJsFilePath = path.join(servicePath, 'serverless.js');
105+
fse.ensureFileSync(serverlessJsFilePath);
106+
107+
return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled
108+
.then(serverlessFilePath => {
109+
expect(serverlessFilePath).to.equal(serverlessJsFilePath);
110+
});
111+
});
102112
});
103113

104114
describe('#getPlugins()', () => {

0 commit comments

Comments
 (0)