|
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; |
|
} |