Repository: serverless/serverless (JavaScript)
Date: 2017-06-23 18:09:42+00:00
Task type: Feature

Issue:
Allow whole file javascript variable exports and check for typeof function
# This is a Feature Proposal
Allow whole file javascript variable exports.

## Description

1. Currently entire yml and json files can be referenced in the serverless.yml file like so:

`custom: ${file(../myCustomFile.yml)} # You can reference the entire file`

However, there are some specific limitations and requirements when it comes to javascript files I find unnecessary.

Attempting to reference a javascript file standalone like...

`custom: ${file(../myCustomFile.js)}`

...throws an obscure error about `split` not being defined. This is because the currently implementation assumes that `:` followed by a property will exist.

While this can be overcome by exporting a named module in your javascript file, it is inconsistent with other supported forms of referencing variables.

2. The other expectation for referencing variables in javascript files that is not well documented is that it needs to export a function. A simple `typeof` check with a reasonable error message can make this more clear.

This issue requests that referencing entire javascript files be supported and that a reasonable error message be shown if that file does not export a function as expected.

Similar or dependent issues:
* NA

## Additional Data

* ***Serverless Framework Version you're using***: 1.16.0
* ***Operating System***: macOS
* ***Stack Trace***: 
```
TypeError: Cannot read property 'split' of undefined
    at Variables.getValueFromFile (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:235:26)
    at Variables.getValueFromSource (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:173:19)
    at property.match.forEach (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:95:40)
    at Array.forEach (native)
    at Variables.populateProperty (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:86:43)
    at deepMapValues (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:66:45)
    at deepMapValues (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:61:14)
    at deepMapValuesIteratee (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:51:25)
    at /home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/node_modules/lodash/lodash.js:13379:38
    at /home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/node_modules/lodash/lodash.js:4944:15
    at baseForOwn (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/node_modules/lodash/lodash.js:3001:24)
    at Function.mapValues (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/node_modules/lodash/lodash.js:13378:7)
    at deepMapValues (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:59:39)
    at deepMapValuesIteratee (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:51:25)
    at /home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/node_modules/lodash/lodash.js:13379:38
    at /home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/node_modules/lodash/lodash.js:4944:15
    at baseForOwn (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/node_modules/lodash/lodash.js:3001:24)
    at Function.mapValues (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/node_modules/lodash/lodash.js:13378:7)
    at deepMapValues (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:59:39)
    at Variables.populateObject (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:64:5)
    at Variables.populateService (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/classes/Variables.js:40:17)
    at Serverless.run (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/lib/Serverless.js:92:27)
    at serverless.init.then (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/bin/serverless:30:50)
From previous event:
    at runCallback (timers.js:672:20)
    at tryOnImmediate (timers.js:645:5)
    at processImmediate [as _immediateCallback] (timers.js:617:5)
From previous event:
    at __dirname (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/bin/serverless:18:28)
    at Object.<anonymous> (/home/travis/.nvm/versions/node/v6.10.3/lib/node_modules/serverless/bin/serverless:34:4)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)
    at run (bootstrap_node.js:390:7)
    at startup (bootstrap_node.js:150:9)
    at bootstrap_node.js:505:3
```



Issue discussion:

Solution patch:
diff --git a/docs/providers/aws/guide/variables.md b/docs/providers/aws/guide/variables.md
index 9a406dd8077..b0e070abdbf 100644
--- a/docs/providers/aws/guide/variables.md
+++ b/docs/providers/aws/guide/variables.md
@@ -150,25 +150,43 @@ functions:
 In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `schedule` property. It's completely recursive and you can go as deep as you want.
 
 ## Reference Variables in Javascript Files
-To add dynamic data into your variables, reference javascript files by putting `${file(./myFile.js):someModule}` syntax in your `serverless.yml`.  Here's an example:
+
+You can reference JavaScript files to add dynamic data into your variables.
+
+References can be either named or unnamed exports. To use the exported `someModule` in `myFile.js` you'd use the following code `${file(./myFile.js):someModule}`. For an unnamed export you'd write `${file(./myFile.js)}`.
+
+Here are other examples:
 
 ```js
-// myCustomFile.js
-module.exports.hello = () => {
+// scheduleConfig.js
+module.exports.rate = () => {
    // Code that generates dynamic data
    return 'rate (10 minutes)';
 }
 ```
 
+```js
+// config.js
+module.exports = () => {
+  return {
+    property1: 'some value',
+    property2: 'some other value'
+  }
+}
+```
+
 ```yml
 # serverless.yml
 service: new-service
 provider: aws
+
+custom: ${file(./config.js)}
+
 functions:
   hello:
       handler: handler.hello
       events:
-        - schedule: ${file(./myCustomFile.js):hello} # Reference a specific module
+        - schedule: ${file(./scheduleConfig.js):rate} # Reference a specific module
 ```
 
 You can also return an object and reference a specific property.  Just make sure you are returning a valid object and referencing a valid property:
diff --git a/docs/providers/azure/guide/variables.md b/docs/providers/azure/guide/variables.md
index e14f867dc93..dae754aac48 100644
--- a/docs/providers/azure/guide/variables.md
+++ b/docs/providers/azure/guide/variables.md
@@ -56,27 +56,40 @@ way, you can easily change the schedule for all functions whenever you like.
 
 ## Reference Variables in JavaScript Files
 
-To add dynamic data into your variables, reference JavaScript files by putting
-`${file(./myFile.js):someModule}` syntax in your `serverless.yml`.  Here's an
-example:
+You can reference JavaScript files to add dynamic data into your variables.
+
+References can be either named or unnamed exports. To use the exported `someModule` in `myFile.js` you'd use the following code `${file(./myFile.js):someModule}`. For an unnamed export you'd write `${file(./myFile.js)}`.
 
 ```js
-// myCustomFile.js
-module.exports.hello = () => {
+// scheduleConfig.js
+module.exports.cron = () => {
    // Code that generates dynamic data
    return 'cron(0 * * * *)';
 }
 ```
 
+```js
+// config.js
+module.exports = () => {
+  return {
+    property1: 'some value',
+    property2: 'some other value'
+  }
+}
+```
+
 ```yml
 # serverless.yml
 service: new-service
 provider: azure
+
+custom: ${file(./config.js)}
+
 functions:
   hello:
       handler: handler.hello
       events:
-        - timer: ${file(./myCustomFile.js):hello} # Reference a specific module
+        - timer: ${file(./scheduleConfig.js):cron} # Reference a specific module
 ```
 
 You can also return an object and reference a specific property.  Just make sure
diff --git a/docs/providers/google/guide/variables.md b/docs/providers/google/guide/variables.md
index 4aed418da26..95d51d10521 100644
--- a/docs/providers/google/guide/variables.md
+++ b/docs/providers/google/guide/variables.md
@@ -52,29 +52,43 @@ In the above example you're setting a global event resource for all functions by
 
 ## Reference Variables in JavaScript Files
 
-To add dynamic data into your variables, reference javascript files by putting `${file(./myFile.js):someModule}` syntax in your `serverless.yml`.  Here's an example:
+You can reference JavaScript files to add dynamic data into your variables.
+
+References can be either named or unnamed exports. To use the exported `someModule` in `myFile.js` you'd use the following code `${file(./myFile.js):someModule}`. For an unnamed export you'd write `${file(./myFile.js)}`.
 
 ```javascript
-// myCustomFile.js
-module.exports.resource = () => {
+// resources.js
+module.exports.topic = () => {
    // Code that generates dynamic data
    return 'projects/*/topics/my-topic';
 }
 ```
 
+```js
+// config.js
+module.exports = () => {
+  return {
+    property1: 'some value',
+    property2: 'some other value'
+  }
+}
+```
+
 ```yml
 # serverless.yml
 service: new-service
 
 provider: google
 
+custom: ${file(./config.js)}
+
 functions:
   first:
     handler: pubSub
     events:
       - event:
           eventType: providers/cloud.pubsub/eventTypes/topics.publish
-          resource: ${file(./myCustomFile.js):resource} # Reference a specific module
+          resource: ${file(./resources.js):topic} # Reference a specific module
 ```
 
 You can also return an object and reference a specific property. Just make sure you are returning a valid object and referencing a valid property:
diff --git a/docs/providers/openwhisk/guide/variables.md b/docs/providers/openwhisk/guide/variables.md
index bcc711cd7a7..eb7d60b971e 100644
--- a/docs/providers/openwhisk/guide/variables.md
+++ b/docs/providers/openwhisk/guide/variables.md
@@ -106,25 +106,41 @@ functions:
 In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `schedule` property. It's completely recursive and you can go as deep as you want.
 
 ## Reference Variables in Javascript Files
-To add dynamic data into your variables, reference javascript files by putting `${file(./myFile.js):someModule}` syntax in your `serverless.yml`.  Here's an example:
+
+You can reference JavaScript files to add dynamic data into your variables.
+
+References can be either named or unnamed exports. To use the exported `someModule` in `myFile.js` you'd use the following code `${file(./myFile.js):someModule}`. For an unnamed export you'd write `${file(./myFile.js)}`.
 
 ```js
-// myCustomFile.js
-module.exports.hello = () => {
+// scheduleConfig.js
+module.exports.cron = () => {
    // Code that generates dynamic data
    return 'cron(0 * * * *)';
 }
 ```
 
+```js
+// config.js
+module.exports = () => {
+  return {
+    property1: 'some value',
+    property2: 'some other value'
+  }
+}
+```
+
 ```yml
 # serverless.yml
 service: new-service
 provider: openwhisk
+
+custom: ${file(./config.js)}
+
 functions:
   hello:
       handler: handler.hello
       events:
-        - schedule: ${file(./myCustomFile.js):hello} # Reference a specific module
+        - schedule: ${file(./scheduleConfig.js):cron} # Reference a specific module
 ```
 
 You can also return an object and reference a specific property.  Just make sure you are returning a valid object and referencing a valid property:
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 6b575609bd3..40c32bc0c4e 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -231,9 +231,25 @@ class Variables {
     // Process JS files
     if (fileExtension === 'js') {
       const jsFile = require(referencedFileFullPath); // eslint-disable-line global-require
-      let jsModule = variableString.split(':')[1];
-      jsModule = jsModule.split('.')[0];
-      valueToPopulate = jsFile[jsModule]();
+      const variableArray = variableString.split(':');
+      let returnValueFunction;
+      if (variableArray[1]) {
+        let jsModule = variableArray[1];
+        jsModule = jsModule.split('.')[0];
+        returnValueFunction = jsFile[jsModule];
+      } else {
+        returnValueFunction = jsFile;
+      }
+
+      if (typeof returnValueFunction !== 'function') {
+        throw new this.serverless.classes
+          .Error([
+            'Invalid variable syntax when referencing',
+            ` file "${referencedFileRelativePath}".`,
+            ' Check if your javascript is exporting a function that returns a value.',
+          ].join(''));
+      }
+      valueToPopulate = returnValueFunction();
 
       return BbPromise.resolve(valueToPopulate).then(valueToPopulateResolved => {
         let deepProperties = variableString.replace(matchedFileRefString, '');


Test patch:
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index e06bea7ab75..217d03c06fb 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -639,6 +639,36 @@ describe('Variables', () => {
         });
     });
 
+    it('should populate an entire variable exported by a javascript file', () => {
+      const serverless = new Serverless();
+      const SUtils = new Utils();
+      const tmpDirPath = testUtils.getTmpDirPath();
+      const jsData = 'module.exports=function(){return { hello: "hello world" };};';
+
+      SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
+
+      serverless.config.update({ servicePath: tmpDirPath });
+
+      return serverless.variables.getValueFromFile('file(./hello.js)')
+        .then(valueToPopulate => {
+          expect(valueToPopulate.hello).to.equal('hello world');
+        });
+    });
+
+    it('should thow if property exported by a javascript file is not a function', () => {
+      const serverless = new Serverless();
+      const SUtils = new Utils();
+      const tmpDirPath = testUtils.getTmpDirPath();
+      const jsData = 'module.exports={ hello: "hello world" };';
+
+      SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
+
+      serverless.config.update({ servicePath: tmpDirPath });
+
+      expect(() => serverless.variables
+        .getValueFromFile('file(./hello.js)')).to.throw(Error);
+    });
+
     it('should populate deep object from a javascript file', () => {
       const serverless = new Serverless();
       const SUtils = new Utils();