Skip to content

Instantly share code, notes, and snippets.

@developit
Last active May 22, 2020 12:36
Show Gist options
  • Save developit/892ae47513c891b53806e98f9b166359 to your computer and use it in GitHub Desktop.
Save developit/892ae47513c891b53806e98f9b166359 to your computer and use it in GitHub Desktop.

resolve(), with Conditional Export Maps

This is a version of the resolve module that is extended with support for Conditional Export Maps, including Package Exports as shipped in Node.

The resolve module powers module resolution in Webpack, Rollup and Browserify.

API

The module maintains the same API and functionality as the original resolve() module.

Two new options are available in the existing options parameter object:

  • conditions: an Array of conditional environment keys, in order of precedence. The first match is used.
  • allowFallback: Boolean indicating whether unmatched export maps should fall back to legacy resolution.

Usage Example

Given a package foo defined at node_modules/foo/package.json:

{
  "exports": {
    ".": "./index.js",
    "./bar": {
      "esmodules": "./bar/modern.mjs",
      "default": "./bar/legacy.cjs"
    }
  }
}

We can resolve as follows:

const resolve = require('resolve-with-conditional-export-maps');

// The environment keys to use:
const conditions = ['esmodules', 'default'];

// resolve a subpackage meant for <script type=module> usage:
resolve('foo/bar', { conditions }, (err, path) => {
  console.log(path);  // node_modules/foo/bar/modern.mjs
});

// synchronous resolution also works:
resolve.sync('foo', { conditions }); // node_modules/foo/index.js
const resolve = require('resolve');
/**
* A version of the resolve module that supports Conditional Export Maps.
* https://nodejs.org/dist/latest-v12.x/docs/api/esm.html#esm_package_exports
*
* Resolve (npm.im/resolve) powers module resolution in Webpack, Rollup and Browserify.
*
* Adds two new options to the existing options parameters:
* @param {string[]} [conditions='default'] - Conditional environment keys, in precedence order. The first match is used.
* @param {boolean} [allowFallback=false] - Whether unmatched export maps should fall back to legacy resolution.
*
* @example
* // node_modules/foo/package.json:
* { "exports": {
* ".": "./index.js",
* "./bar": {
* "esmodules": "./bar/modern.mjs",
* "default": "./bar/legacy.cjs"
* }
* } }
*
* // example.js:
* resolveWithConditionalMaps(
* 'foo/bar',
* { conditions: ['esmodules','default'] },
* (err, path) => {
* console.log(path); // node_modules/foo/bar/modern.mjs
* }
* )
*/
module.exports = function resolveWithConditionalMaps (id, opts, cb) {
applyConditionalMaps(resolve, id, opts, cb);
};
module.exports.sync = function () {
return applyConditionalMaps(resolve.sync, id, opts);
};
function applyConditionalMaps(resolveImpl, id, opts, cb) {
const {
conditions = ['default'],
allowFallback = false,
pathFilter,
...options
} = opts;
options.pathFilter = function conditionalMapsFilter (pkg, path, relativePath) {
const exports = pkg.exports;
if (!exports) {
return pathFilter ? pathFilter(pkg, path, relativePath) : undefined;
}
if (relativePath[0] !== '.') {
relativePath = './' + relativePath;
}
// { "exports": "./main.js" }
if (typeof exports === 'string') {
return pathFilter && pathFilter(pkg, path, exports) || exports;
}
// { "exports": { "%ENV%": %MAPPING% } }
for (let key in exports) {
const value = exports[key];
if (key === relativePath) {
const mapping = evaluateMapping(value, conditions);
return pathFilter && (pkg, path, mapping) || mapping;
}
}
// Node's decision was to *not* fall back to CJS resolution here.
if (allowFallback) {
return resolveImpl(relativePath, opts, cb);
}
if (pathFilter) return pathFilter(pkg, path, relativePath);
};
return resolveImpl(id, options, cb);
}
const HOP = Object.prototype.hasOwnProperty;
/**
* Evaluate a Conditional Export Map.
* @see https://nodejs.org/api/esm.html#esm_conditional_exports
* @see https://github.com/jkrems/proposal-pkg-exports#2-conditional-mapping
*/
function evaluateMapping (value, conditions) {
// "./a.js"
if (typeof value !== 'object' || value == null) {
return value;
}
// ["not:valid", "./a.js"]
if (Array.isArray(value)) {
for (const mapped of value) {
const result = evaluateMapping(mapped, conditions);
if (result) return result;
}
return null;
}
// { "browser": "./browser.js", "node": "./a.js" }
for (const env of conditions) {
if (HOP.call(value, env)) {
return value[env];
}
}
return null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment