Skip to content

Commit

Permalink
Merge pull request #9 from ten-lac/feature/custom-server-file-location
Browse files Browse the repository at this point in the history
Custom Server File Location
  • Loading branch information
devinivy committed Jul 2, 2020
2 parents 22ee0e4 + 91f8248 commit 414baf8
Show file tree
Hide file tree
Showing 22 changed files with 250 additions and 17 deletions.
9 changes: 7 additions & 2 deletions API.md
Expand Up @@ -92,13 +92,13 @@ server.lambda({
```

## The Serverless plugin
Lalalambda takes no options when used as a Serverless plugin. Currently the plugin only supports the [`aws` Serverless provider](https://serverless.com/framework/docs/providers/aws/), and each function deployed via lalalambda must use the `nodejs8.10` runtime or newer (`nodejs12.x` is recommended). The plugin is responsible for:
Currently the plugin only supports the [`aws` Serverless provider](https://serverless.com/framework/docs/providers/aws/), and each function deployed via lalalambda must use the `nodejs8.10` runtime or newer (`nodejs12.x` is recommended). The plugin is responsible for:

1. Configuring the project's Serverless service based upon relevant lambda and route configurations made within hapi.

2. Writing lambda handler files during packaging, deployment, local invocation, etc., and later cleaning them up. These files will be written in your project root's `_lalalambda/` directory.

In order to interoperate with your hapi server, it is expected that `server.js` or `server/index.js` export an async function named `deployment` returning your configured hapi server. This server should have the [lalalambda hapi plugin](#the-hapi-plugin) registered.
In order to interoperate with your hapi server, it is expected that `server.js` or `server/index.js` export an async function named `deployment` returning your configured hapi server. This server should have the [lalalambda hapi plugin](#the-hapi-plugin) registered. The path to `server.js` can also be customized through the `custom.lalalambda` config section as shown below.

A minimal Serverless [config](https://serverless.com/framework/docs/providers/aws/guide/serverless.yml/) utilizing lalalambda will look like this:

Expand All @@ -110,6 +110,11 @@ provider:
name: aws
runtime: nodejs12.x

# optional
custom:
lalalambda:
serverPath: some/relative/path/to/server.js

plugins:
- lalalambda
```
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -185,6 +185,16 @@ Lalalambda is one package that doubles as 1. a hapi plugin and 2. a [Serverless
- lalalambda
```

There is also an optional configuration for declaring the path to the server file.

```yaml
# serverless.yml

custom:
lalalambda:
serverPath: 'src/my-server.js' # This is always relative to the serverless.yml file.
```

3. Register lalalambda to your hapi server.

> If you're using [the pal boilerplate](https://github.com/hapipal/boilerplate) then simply add lalalambda to your [manifest's](https://github.com/hapipal/boilerplate/blob/pal/server/manifest.js) `plugins` section.
Expand Down
44 changes: 30 additions & 14 deletions lib/serverless.js
Expand Up @@ -49,9 +49,7 @@ exports.Plugin = class {

async initialize() {

const { servicePath } = this.sls.config;

const server = this.server = await internals.getServer(servicePath);
const server = this.server = await internals.getServer(this.serverPath);

Hoek.assert(server.plugins.lalalambda, 'Lalalambda needs to be registered as a plugin on your hapi server.');
}
Expand Down Expand Up @@ -100,18 +98,35 @@ exports.Plugin = class {
Hoek.assert(server, 'Lalalambda must be initialized.');

const { servicePath } = this.sls.config;
const { lambdas } = server.plugins.lalalambda;
const buildFolderPath = Path.join(servicePath, BUILD_FOLDER);

await this.cleanup();
await internals.mkdir(Path.join(servicePath, BUILD_FOLDER));
await internals.mkdir(buildFolderPath);

const { lambdas } = server.plugins.lalalambda;

for (const id of lambdas.keys()) {
await internals.writeFile(
Path.join(servicePath, BUILD_FOLDER, `${id}.js`),
internals.entrypoint(id)
Path.join(buildFolderPath, `${id}.js`),
// Path listed in entrypoint should be relative
// in order for the code to remain portable.
internals.entrypoint(id, Path.relative(buildFolderPath, this.serverPath))
);
}
}

get serverPath() {

const { servicePath } = this.sls.config;

const serverPath = Hoek.reach(
this.sls,
['service', 'custom', 'lalalambda', 'serverPath'],
{ default: 'server' }
);

return Path.resolve(servicePath, serverPath);
}
};

exports.handler = (id, path) => {
Expand Down Expand Up @@ -151,15 +166,13 @@ internals.writeFile = Util.promisify(Fs.writeFile);

internals.rimraf = Util.promisify(Rimraf);

internals.getServer = async (root) => {

const path = Path.join(root, 'server');
internals.getServer = async (path) => {

try {

const srv = require(path);

Hoek.assert(typeof srv.deployment === 'function', `No server found! The current project must export { deployment: async () => server } from ${root}/server.`);
Hoek.assert(typeof srv.deployment === 'function', `No server found! The current project must export { deployment: async () => server } from ${path}.`);

const server = await srv.deployment();

Expand All @@ -169,17 +182,20 @@ internals.getServer = async (root) => {
}
catch (err) {

Hoek.assert(err.code !== 'MODULE_NOT_FOUND' || !err.message.includes(`'${path}'`), `No server found! The current project must export { deployment: async () => server } from ${root}/server.`);
Hoek.assert(err.code !== 'MODULE_NOT_FOUND' || !err.message.includes(`'${path}'`), `No server found! The current project must export { deployment: async () => server } from ${path}.`);

throw err;
}
};

// eslint-disable-next-line @hapi/hapi/scope-start
internals.entrypoint = (id) => `'use strict';
internals.entrypoint = (id, path) => `'use strict';
const Path = require('path');
const Lalalambda = require('lalalambda');
exports.handler = Lalalambda.handler('${id}', Path.resolve(__dirname, '..'));
exports.handler = Lalalambda.handler('${internals.esc(id)}', Path.resolve(__dirname, '${internals.esc(path)}'));
`;

// Allow slashes and single-quotes in filenames
internals.esc = (str) => str.replace(/(\\|')/g, '\\$1');
18 changes: 18 additions & 0 deletions test/closet/invoke-custom-server-path-escaped/'.js
@@ -0,0 +1,18 @@
'use strict';

const { Hapi } = require('../../helpers');
const Lalalambda = require('../../..');

exports.deployment = async () => {

const server = Hapi.server();

await server.register(Lalalambda);

server.lambda({
id: 'invoke-lambda',
handler: () => ({ success: 'invoked' })
});

return server;
};
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('../../../..');
12 changes: 12 additions & 0 deletions test/closet/invoke-custom-server-path-escaped/serverless.yaml
@@ -0,0 +1,12 @@
service: my-service

provider:
name: aws
runtime: nodejs12.x

custom:
lalalambda:
serverPath: "'.js"

plugins:
- lalalambda
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('../../../..');
12 changes: 12 additions & 0 deletions test/closet/invoke-custom-server-path/serverless.yaml
@@ -0,0 +1,12 @@
service: my-service

provider:
name: aws
runtime: nodejs12.x

custom:
lalalambda:
serverPath: 'src/server.js'

plugins:
- lalalambda
18 changes: 18 additions & 0 deletions test/closet/invoke-custom-server-path/src/server.js
@@ -0,0 +1,18 @@
'use strict';

const { Hapi } = require('../../../helpers');
const Lalalambda = require('../../../..');

exports.deployment = async () => {

const server = Hapi.server();

await server.register(Lalalambda);

server.lambda({
id: 'invoke-lambda',
handler: () => ({ success: 'invoked' })
});

return server;
};
2 changes: 2 additions & 0 deletions test/closet/invoke/index.js
@@ -0,0 +1,2 @@
// This exists to ensure that when serverPath isn't specified, this file
// doesn't get picked-up as the default server export rather than server.js
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('../../../..');
@@ -0,0 +1,5 @@
'use strict';

const Helpers = require('../../../helpers');

module.exports = Helpers.OfflineMock;
9 changes: 9 additions & 0 deletions test/closet/missing-server-file/serverless.yaml
@@ -0,0 +1,9 @@
service: my-service

provider:
name: aws
runtime: nodejs12.x

plugins:
- lalalambda
- serverless-offline-mock
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('../../../..');
@@ -0,0 +1,5 @@
'use strict';

const Helpers = require('../../../helpers');

module.exports = Helpers.OfflineMock;
@@ -0,0 +1,13 @@
service: my-service

provider:
name: aws
runtime: nodejs12.x

custom:
lalalambda:
serverPath: 'src/server.js'

plugins:
- lalalambda
- serverless-offline-mock
13 changes: 13 additions & 0 deletions test/closet/server-file-location-with-file-extension/src/server.js
@@ -0,0 +1,13 @@
'use strict';

const { Hapi } = require('../../../helpers');
const Lalalambda = require('../../../../lib');

exports.deployment = async () => {

const server = Hapi.server();

await server.register(Lalalambda);

return server;
};
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('../../../..');
@@ -0,0 +1,5 @@
'use strict';

const Helpers = require('../../../helpers');

module.exports = Helpers.OfflineMock;
@@ -0,0 +1,13 @@
service: my-service

provider:
name: aws
runtime: nodejs12.x

custom:
lalalambda:
serverPath: 'src/server'

plugins:
- lalalambda
- serverless-offline-mock
@@ -0,0 +1,13 @@
'use strict';

const { Hapi } = require('../../../helpers');
const Lalalambda = require('../../../../lib');

exports.deployment = async () => {

const server = Hapi.server();

await server.register(Lalalambda);

return server;
};
51 changes: 50 additions & 1 deletion test/index.js
Expand Up @@ -1248,6 +1248,33 @@ describe('Lalalambda', () => {
});
});

it('fails when server file does not exist', async () => {

const serverless = Helpers.makeServerless('missing-server-file', []);

await serverless.init();

await expect(serverless.run()).to.reject(`No server found! The current project must export { deployment: async () => server } from ${Path.join(__dirname, '/closet/missing-server-file/server.')}`);
});

it('can load the server file with file extension from a custom path', async () => {

const serverless = Helpers.makeServerless('server-file-location-with-file-extension', []);

await serverless.init();

await expect(serverless.run()).to.not.reject();
});

it('can load the server file without file extension from a custom path', async () => {

const serverless = Helpers.makeServerless('server-file-location-without-file-extension', []);

await serverless.init();

await expect(serverless.run()).to.not.reject();
});

it('fails when deployment does not exist.', async () => {

const serverless = Helpers.makeServerless('bad-deployment-missing', []);
Expand Down Expand Up @@ -1286,6 +1313,28 @@ describe('Lalalambda', () => {
expect(output).to.contain(`"success": "invoked"`);
});

it('can locally invoke a lambda registered by hapi to a custom serverPath.', async () => {

const serverless = Helpers.makeServerless('invoke-custom-server-path', ['invoke', 'local', '--function', 'invoke-lambda']);

await serverless.init();

const output = await serverless.run();

expect(output).to.contain(`"success": "invoked"`);
});

it('can locally invoke a lambda registered by hapi to a custom serverPath containing a single quote.', async () => {

const serverless = Helpers.makeServerless('invoke-custom-server-path-escaped', ['invoke', 'local', '--function', 'invoke-lambda']);

await serverless.init();

const output = await serverless.run();

expect(output).to.contain(`"success": "invoked"`);
});

it('invokes lambdas registered by hapi with server and bound context.', async () => {

const serverless = Helpers.makeServerless('invoke-context', ['invoke', 'local', '--function', 'invoke-context-lambda', '--data', '{"an":"occurrence"}']);
Expand Down Expand Up @@ -1396,7 +1445,7 @@ describe('Lalalambda', () => {
const Path = require('path');
const Lalalambda = require('lalalambda');
exports.handler = Lalalambda.handler('package-lambda', Path.resolve(__dirname, '..'));
exports.handler = Lalalambda.handler('package-lambda', Path.resolve(__dirname, '../server'));
`));

const cfFile = await readFile(Path.join(__dirname, 'closet', 'package', '.serverless', 'cloudformation-template-update-stack.json'));
Expand Down

0 comments on commit 414baf8

Please sign in to comment.