Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading