Skip to content

Commit

Permalink
Merge pull request #14 from groupon/dbushong/feature/master/kill-esm-mod
Browse files Browse the repository at this point in the history
stop using `esm` for ES module loading
  • Loading branch information
dbushong authored Dec 10, 2019
2 parents 5bcdfbe + 9b5b703 commit 4ab103d
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 138 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/node_modules
/tmp
/lib/esm/import.js
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"extends": "groupon/node8"
"extends": "groupon/node10"
}
11 changes: 6 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
language: node_js
node_js:
- '8'
- '10'
- '12'
env:
- NODE_OPTIONS=
- NODE_OPTIONS=--experimental-modules
deploy:
- provider: script
script: ./node_modules/.bin/nlm release
script: npx nlm release
skip_cleanup: true
'on':
branch: master
node: '10'
before_install:
- npm i -g npm@^6
node: '12'
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ This function returns an array with one entry for each interface file:
relative to the app's root directory.
* `group`: The directory the file was found in.

Note that `.mjs` file support requires you to be using a version of node with
builtin support for ES Modules. Currently this is Node 10+ with
the `--experimental-modules` flag. Node 10.x seems to experience
segfaults under certain conditions, so we recommend 12+.

### Registry

A set of three scopes, in order of nesting:
Expand Down
40 changes: 40 additions & 0 deletions lib/esm/import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2019, Groupon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of GROUPON nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
'use strict';

// put this in its own file so that it can barf on require if needed

/**
* @param {string} id
* @returns {Promise<any>}
*/
module.exports = id => import(id);
48 changes: 48 additions & 0 deletions lib/esm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2019, Groupon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of GROUPON nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

'use strict';

const debug = require('debug')('esm');

/** @type {(id: string) => Promise<any>} */
let importESM;
try {
importESM = require('./import');
} catch (err) {
debug('failed to load esm import: ', err);
importESM = () => Promise.reject(new Error('Not supported'));
}

exports.importESM = importESM;
exports.supportsESM = () =>
importESM('../../package.json').then(() => true, () => false);
100 changes: 34 additions & 66 deletions lib/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,42 +33,33 @@
'use strict';

const path = require('path');
const Module = require('module');
// @ts-ignore
const { createRequireFromPath, createRequire } = require('module');
const util = require('util');
const { URL } = require('url');

const debug = require('debug')('nilo:project');
// @ts-ignore
const esm = require('esm');
// @ts-ignore
const globCallback = require('glob');

const glob = util.promisify(globCallback);

// @ts-ignore
const rawModulePaths = Module['_nodeModulePaths'];
const getModulePaths = /** @type {(from: string) => string[]} */ (rawModulePaths);
// createRequireFromPath is deprecated Node 10.x; createRequire is official 12+
/** @type {typeof createRequireFromPath} */
const createReq = createRequire || createRequireFromPath;

const { importESM } = require('./esm');

/**
* @typedef {import('./typedefs').InterfaceFile} InterfaceFile
* @param {string} reSTR
*/
function escapeRE(reSTR) {
return reSTR.replace(/[$^*()+\[\]{}\\|.?]/g, '\\$&');
}

/**
* @param {object} esmResult
* @param {string} specifier
* @typedef {import('./typedefs').InterfaceFile} InterfaceFile
*/
function guessNamespace(esmResult, specifier) {
if (specifier.endsWith('.mjs')) return esmResult;

// "Heuristic" to figure out if it kinda looks like a namespace
if (
esmResult !== null &&
typeof esmResult === 'object' &&
'default' in esmResult
)
return esmResult;

return { default: esmResult };
}

class Project {
/**
Expand All @@ -79,39 +70,20 @@ class Project {
this.root = appDirectory;

const appPath = path.resolve(appDirectory, 'app');
this.app = new Module('<app>');
this.app.filename = appPath;
this.app.paths = getModulePaths(appPath);
this.require = createReq(appPath);

const frameworkPath = path.resolve(frameworkDirectory, 'app');
this.framework = new Module('<framework>');
this.framework.filename = frameworkPath;
this.framework.paths = getModulePaths(frameworkPath);
this.requireBundled = createReq(frameworkPath);

this._pkgJson = null;
this._globCache = {};
this._globStatCache = {};

this._importESM = esm(this.app, {
cjs: {
cache: false,
esModule: false,
extensions: false,
mutableNamespace: false,
namedExports: false,
paths: false,
vars: false,
dedefault: false,
topLevelReturn: false,
},
mode: 'strict',
});
}

/**
* @param {string} pattern
* @param {object} [options]
* @returns {string[]}
* @returns {Promise<string[]>}
*/
cachedGlob(pattern, options = {}) {
return glob(
Expand Down Expand Up @@ -188,9 +160,22 @@ class Project {
* @param {string} id
* @returns {Promise<unknown>}
*/
import(id) {
const esmResult = this._importESM(id);
return guessNamespace(esmResult, id);
async import(id) {
debug('import', id);
// if it's a package spec, assume we can't load it as a ES module
// for now
if (!/^(\.\.?)?\//.test(id)) return { default: this.require(id) };

try {
return await importESM(
/^\.\.?\//.test(id)
? new URL(id, `file://${this.root}/app`).toString()
: id
);
} catch (err) {
if (err.message !== 'Not supported') throw err;
return { default: this.require(id) };
}
}

/**
Expand All @@ -204,21 +189,13 @@ class Project {
// Still throw errors unrelated to finding the module
if (e.code !== 'MODULE_NOT_FOUND') throw e;
// Do *not* ignore failing requires of subsequent files
if (!e.message.includes(`'${id}'`)) throw e;
const re = new RegExp(`(^|[\\s'"])${escapeRE(id)}([\\s'"]|$)`);
if (!re.test(e.message)) throw e;

return null;
}
}

/**
* @template T
* @param {string} id
* @returns {T}
*/
require(id) {
return this.app.require(id);
}

/**
* @template T
* @param {string} id
Expand All @@ -237,15 +214,6 @@ class Project {
}
}

/**
* @template T
* @param {string} id
* @returns {T}
*/
requireBundled(id) {
return this.framework.require(id);
}

/**
* @template T
* @param {string} id
Expand Down
23 changes: 6 additions & 17 deletions lib/registry/injector.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,7 @@ const { parseDependencyQuery } = require('./query');
/** @typedef {import('../typedefs').DependencyDescriptor} DependencyDescriptor */
/** @typedef {import('../typedefs').DependencyQuery} DependencyQuery */

const INSPECT = util.inspect.custom || Symbol.for('nodejs.util.inspect.custom');

/**
* @param {Injector} injector
*/
function inspectProvider(injector) {
// @ts-ignore
return `Provider { ${injector[INSPECT]()} }`;
}
const INSPECT = util.inspect.custom;

const PROVIDER_HANDLER = {
/**
Expand All @@ -59,17 +51,14 @@ const PROVIDER_HANDLER = {
get(injector, key) {
switch (key) {
case 'constructor':
return injector.constructor;

case 'get':
return injector.get;

case 'keys':
return injector.keys;
case 'scope':
return injector[key];

case INSPECT: {
return inspectProvider.bind(null, injector);
}
case INSPECT:
// @ts-ignore
return injector[INSPECT];

default:
return injector.get(key);
Expand Down
5 changes: 0 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"dependencies": {
"commander": "^2.19.0",
"debug": "^4.1.1",
"esm": "^3.2.20",
"glob": "^7.1.3"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit 4ab103d

Please sign in to comment.