Skip to content

Commit

Permalink
feat: support package.json exports field
Browse files Browse the repository at this point in the history
related to #17
  • Loading branch information
3cp committed Dec 22, 2024
1 parent 56fdd5e commit a800eb8
Show file tree
Hide file tree
Showing 3 changed files with 577 additions and 9 deletions.
5 changes: 4 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,11 @@ module.exports = class Bundler {
});
}

const parsedRequiredBy = requiredBy.map(id => parse(mapId(id, this._paths)));
const isLocalRequire = parsedRequiredBy.findIndex(parsed => parsed.parts[0] === packageName) !== -1;

return this.packageReaderFor(stub || {name: packageName})
.then(reader => resource ? reader.readResource(resource) : reader.readMain())
.then(reader => resource ? reader.readResource(resource, isLocalRequire) : reader.readMain())
.then(unit => this.capture(unit))
.catch(err => {
error('Resolving failed for module ' + bareId);
Expand Down
142 changes: 135 additions & 7 deletions lib/package-reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module.exports = class PackageReader {
this.name = metadata.name;
this.version = metadata.version || 'N/A';
this.browserReplacement = _browserReplacement(metadata.browser);
this.exportsReplacement = _exportsReplacement(metadata.exports);

return this._main(metadata)
// fallback to "index.js" even when it's missing.
Expand Down Expand Up @@ -65,7 +66,7 @@ module.exports = class PackageReader {
.then(() => this._readFile(this.mainPath));
}

readResource(resource) {
readResource(resource, isLocalRequire = false) {
return this.ensureMainPath().then(() => {
let parts = this.parsedMainId.parts;
let len = parts.length;
Expand All @@ -81,8 +82,48 @@ module.exports = class PackageReader {

let fullResource = resParts.join('/');

const replacement = this.browserReplacement['./' + fullResource] ||
let replacement;

// exports subpath is designed for outside require.
if (!isLocalRequire) {
if (('./' + fullResource) in this.exportsReplacement) {
replacement = this.exportsReplacement['./' + fullResource];
} else if (('./' + fullResource + '.js') in this.exportsReplacement) {
replacement = this.exportsReplacement['./' + fullResource + '.js'];
}

if (replacement === null) {
throw new Error(`Resource ${this.name + '/' + resource} is not allowed to be imported (${this.name} package.json exports definition ${JSON.stringify(this.exportsReplacement)}).`);
}

if (!replacement) {
// Try wildcard replacement
for (const key in this.exportsReplacement) {
const starIndex = key.indexOf('*');
if (starIndex !== -1) {
const prefix = key.slice(2, starIndex); // remove ./
const subfix = key.slice(starIndex + 1);
if (fullResource.startsWith(prefix) && fullResource.endsWith(subfix)) {

const target = this.exportsReplacement[key];
if (target && target.includes('*')) {
const flexPart = fullResource.slice(prefix.length, fullResource.length - subfix.length);
replacement = target.replace('*', flexPart);
} else {
replacement = target;
}
break;
}
}
}
}
}

if (!replacement) {
replacement = this.browserReplacement['./' + fullResource] ||
this.browserReplacement['./' + fullResource + '.js'];
}

if (replacement) {
// replacement is always local, remove leading ./
fullResource = replacement.slice(2);
Expand Down Expand Up @@ -254,13 +295,10 @@ module.exports = class PackageReader {
}

_main(metadata, dirPath = '') {
// try 1.browser > 2.module > 3.main
// try 1. exports > 2.browser > 3.module > 4.main
// the order is to target browser.
// it probably should use different order for electron app
// for electron 1.module > 2.browser > 3.main
// note path.join also cleans up leading './'.
const mains = [];

if (typeof metadata.dumberForcedMain === 'string') {
// dumberForcedMain is not in package.json.
// it is the forced main override in dumber config,
Expand All @@ -269,6 +307,12 @@ module.exports = class PackageReader {
// note there is no fallback to other browser/module/main fields.
mains.push({field: 'dumberForcedMain', path: path.join(dirPath, metadata.dumberForcedMain)});
} else {

const exportsMain = _exportsMain(metadata.exports);
if (typeof exportsMain === 'string') {
mains.push({field: 'exports', path: path.join(dirPath, exportsMain)});
}

if (typeof metadata.browser === 'string') {
// use package.json browser field if possible.
mains.push({field: 'browser', path: path.join(dirPath, metadata.browser)});
Expand Down Expand Up @@ -330,7 +374,10 @@ function _browserReplacement(browser) {
// replacement is always local
targetModule = './' + targetModule;
}
replacement[sourceModule] = targetModule;
// Only replace when sourceModule cannot be resolved to targetModule.
if (!nodejsIds(sourceModule).includes(targetModule)) {
replacement[sourceModule] = targetModule;
}
} else {
replacement[sourceModule] = false;
}
Expand All @@ -339,6 +386,87 @@ function _browserReplacement(browser) {
return replacement;
}

function isExportsConditions(obj) {
if (typeof obj !== 'object' || obj === null) return false;
const keys = Object.keys(obj);
return keys.length > 0 && keys[0][0] !== '.';
}

function pickCondition(obj) {
// string or null
if (typeof obj !== 'object' || obj === null) return obj;
let env = process.env.NODE_ENV || '';
if (env === 'undefined') env = '';

// use env (NODE_ENV) to support "development" and "production"
for (const condition of ['import', 'module', 'browser', 'require', env, 'default']) {
// Recursive call to support nested conditions.
if (condition && condition in obj) return pickCondition(obj[condition]);
}

return null;
}

function _exportsMain(exports) {
// string exports field is alternative main
if (!exports || typeof exports === 'string') return exports;

if (isExportsConditions(exports)) {
return pickCondition(exports);
}

if (typeof exports === 'object') {
for (const key in exports) {
if (key === '.' || key !== './') {
return pickCondition(exports[key]);
}
}
}
}

function _exportsReplacement(exports) {
// string exports field is alternative main,
// leave to the main field replacment
if (!exports || typeof exports === 'string') return {};

if (isExportsConditions(exports)) {
// leave to the main field replacment
return {};
}

let replacement = {};

Object.keys(exports).forEach(key => {
// leave {".": ...} to the main field replacment
if (key === '.') return;

if (key[0] !== '.') {
throw new Error("Unexpected exports subpath: " + key);
}

let target = pickCondition(exports[key]);

let sourceModule = filePathToModuleId(key);

if (typeof target === 'string') {
let targetModule = filePathToModuleId(target);
if (!targetModule.startsWith('.')) {
// replacement is always local
targetModule = './' + targetModule;
}

// Only replace when sourceModule cannot be resolved to targetModule.
if (!nodejsIds(sourceModule).includes(targetModule)) {
replacement[sourceModule] = targetModule;
}
} else {
replacement[sourceModule] = null;
}
});

return replacement;
}

function filePathToModuleId(filePath) {
return parse(filePath.replace(/\\/g, '/')).bareId;
}
Expand Down
Loading

0 comments on commit a800eb8

Please sign in to comment.