Skip to content

Commit

Permalink
Feat: Import functions that are written as ES Modules (#140)
Browse files Browse the repository at this point in the history
* feat: add the ability to use a function written as an ES module
  • Loading branch information
lholmquist authored Dec 9, 2022
1 parent b1bafc8 commit 6ac6fff
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 2 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ with the CLI.
npx faas-js-runtime ./index.js
```

### Functions as ES Modules

Functions can be written and imported as ES modules with either the `.mjs` file extenstion or by adding the `type` property to the functions package.json and setting it to `module`.

```js
// index.mjs
const handle = async function(context) => { ... };

// Export the function
export { handle };
```

If using the `type` property, the package.json might look something like this:
```js
{
"name": "moduleName",
"type": "module"
}
```



## Usage as a Module

In the current working directory, there is an `index.js` file like this.
Expand Down
6 changes: 4 additions & 2 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const path = require('path');

const { start, defaults } = require('../');
const { loadFunction } = require('../lib/function-loader.js');
const pkg = require('../package.json');

const ON_DEATH = require('death')({ uncaughtException: true });
Expand All @@ -20,7 +21,7 @@ program.parse(process.argv);

async function runServer(file) {
const programOpts = program.opts();

try {
let server;
let options = {
Expand All @@ -29,7 +30,8 @@ async function runServer(file) {
};

const filePath = extractFullPath(file);
const code = require(filePath);
const code = await loadFunction(filePath);

if (typeof code === 'function') {
server = await start(code, options);
} else if (typeof code.handle === 'function') {
Expand Down
38 changes: 38 additions & 0 deletions lib/function-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const path = require('path');

// This is the way to import es modules inside a CJS module
const dynamicImport = new Function('modulePath', 'return import(modulePath)');

async function loadFunction(filePath) {
if (isESM(filePath)) {
code = await dynamicImport(filePath);
} else {
code = require(filePath);
}

return code;
}

// https://nodejs.org/dist/latest-v18.x/docs/api/packages.html#determining-module-system
// An ESM module can be determined 2 ways
// 1. has the mjs file extention
// 2. type=module in the package.json
function isESM(filePath) {
const pathParsed = path.parse(filePath);

if (pathParsed.ext === '.mjs') {
return true;
}

// find the functions package.json and see if it has a type field
const functionPkg = require(path.join(pathParsed.dir, 'package.json'));
if (functionPkg.type === 'module') {
return true;
}

// Should default to CJS
return false;
}


module.exports = exports = { loadFunction };
3 changes: 3 additions & 0 deletions test/fixtures/cjs-module/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function testFunc(context) {
return context;
};
4 changes: 4 additions & 0 deletions test/fixtures/cjs-module/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "cjs-module",
"version": "1.0.0"
}
21 changes: 21 additions & 0 deletions test/fixtures/esm-module-mjs/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const handle = async (context) => {
// YOUR CODE HERE
context.log.info(JSON.stringify(context, null, 2));

// If the request is an HTTP POST, the context will contain the request body
if (context.method === 'POST') {
return {
body: context.body,
}
// If the request is an HTTP GET, the context will include a query string, if it exists
} else if (context.method === 'GET') {
return {
query: context.query,
}
} else {
return { statusCode: 405, statusMessage: 'Method not allowed' };
}
}

// Export the function
export { handle };
4 changes: 4 additions & 0 deletions test/fixtures/esm-module-mjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "esm-module-mjs",
"version": "1.0.0"
}
21 changes: 21 additions & 0 deletions test/fixtures/esm-module-type-module/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const handle = async context => {
// YOUR CODE HERE
context.log.info(JSON.stringify(context, null, 2));

// If the request is an HTTP POST, the context will contain the request body
if (context.method === 'POST') {
return {
body: context.body,
};
// If the request is an HTTP GET, the context will include a query string, if it exists
} else if (context.method === 'GET') {
return {
query: context.query,
};
} else {
return { statusCode: 405, statusMessage: 'Method not allowed' };
}
};

// Export the function
export { handle };
5 changes: 5 additions & 0 deletions test/fixtures/esm-module-type-module/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "esm-module-type-module",
"version": "1.0.0",
"type": "module"
}
23 changes: 23 additions & 0 deletions test/test-function-loading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const test = require('tape');
const path = require('path');
const { loadFunction } = require('../lib/function-loader.js');

const fixtureDir = path.join(__dirname, 'fixtures');

test('Loads a CJS function', async t => {
const fn = await loadFunction(path.join(fixtureDir, 'cjs-module', 'index.js'));
t.equal(typeof fn, 'function');
t.pass('CJS function loaded');
});

test('Loads an ESM function with an .mjs extension', async t => {
const fn = await loadFunction(path.join(fixtureDir, 'esm-module-mjs', 'index.mjs'));
t.equal(typeof fn.handle, 'function');
t.pass('ESM module with a mjs ext loaded');
});

test('Loads an ESM function with the type=module in the package.json', async t => {
const fn = await loadFunction(path.join(fixtureDir, 'esm-module-type-module', 'index.js'));
t.equal(typeof fn.handle, 'function');
t.pass('ESM module with a type=module in the package.json');
});

0 comments on commit 6ac6fff

Please sign in to comment.