Permalink
Browse files

feat(bundler): fully support package.json browser field

Support (1) alternative main, (2) replace specific files, (3) ignore a module.
Cleanup dependency string ending in '/' or '.js'.

closes #579, #581
  • Loading branch information...
huochunpeng committed Sep 6, 2018
1 parent 1669a6f commit 5bb81d404f4836c49d9bc9b6a8d49f0ccf904833
@@ -4,7 +4,8 @@
// requirejs optimizer.
var transforms = [
require('./stubs'),
require('./defines')
require('./defines'),
require('./replace')
];
/**
@@ -0,0 +1,87 @@
'use strict';
// browser replacement
// https://github.com/defunctzombie/package-browser-field-spec
// see bundled-source.js for more details
// and also dep string cleanup
// remove tailing '/', '.js'
const esprima = require('esprima');
const astMatcher = require('../../ast-matcher').astMatcher;
// it is definitely a named AMD module at this stage
var amdDep = astMatcher('define(__str, [__anl_deps], __any)');
var cjsDep = astMatcher('require(__any_dep)');
module.exports = function stubs(options) {
options = options || {};
return function(context, moduleName, filePath, contents) {
const replacement = options.replacement;
const toReplace = [];
const _find = node => {
if (node.type !== 'Literal') return;
let dep = node.value;
// remove tailing '/'
if (dep.endsWith('/')) {
dep = dep.substr(0, dep.length - 1);
}
// remove tailing '.js', but only when dep is not
// referencing a npm package main
if (dep.endsWith('.js') && !isPackageName(dep)) {
dep = dep.substr(0, dep.length - 3);
}
// browser replacement;
if (replacement && replacement[dep]) {
dep = replacement[dep];
}
if (node.value !== dep) {
toReplace.push({
start: node.range[0],
end: node.range[1],
text: `'${dep}'`
});
}
};
// need node location
const parsed = esprima.parse(contents, {range: true});
const amdMatch = amdDep(parsed);
if (amdMatch) {
amdMatch.forEach(result => {
result.match.deps.forEach(_find);
});
}
const cjsMatch = cjsDep(parsed);
if (cjsMatch) {
cjsMatch.forEach(result => {
_find(result.match.dep);
});
}
// reverse sort by "start"
toReplace.sort((a, b) => b.start - a.start);
toReplace.forEach(r => {
contents = modify(contents, r);
});
return contents;
};
};
function modify(contents, replacement) {
return contents.substr(0, replacement.start) +
replacement.text +
contents.substr(replacement.end);
}
function isPackageName(path) {
if (path.startsWith('.')) return false;
const parts = path.split('/');
// package name, or scope package name
return parts.length === 1 || (parts.length === 2 && parts[0].startsWith('@'));
}
View
@@ -108,8 +108,6 @@ exports.Bundle = class {
getBundledModuleIds() {
let allModuleIds = this.getRawBundledModuleIds();
// add all nodeId compatibility aliases
Object.keys(nodeIdCompatAliases(allModuleIds)).forEach(d => allModuleIds.add(d));
return Array.from(allModuleIds).sort().map(id => {
let matchingPlugin = this.bundler.loaderOptions.plugins.find(p => p.matches(id));
if (matchingPlugin) {
@@ -182,10 +180,6 @@ exports.Bundle = class {
}
let aliases = this.getAliases();
// Solve nodeIdCompat manually, so we can support systemjs and karma test.
// For every module like foo/bar, create aliases foo/bar.js to foo/bar
Object.assign(aliases, nodeIdCompatAliases(this.getRawBundledModuleIds()));
if (Object.keys(aliases).length) {
// a virtual prepend file contains nodejs module aliases
// for instance:
@@ -481,18 +475,3 @@ function uniqueBy(collection, key) {
return seen.hasOwnProperty(k) ? false : (seen[k] = true);
});
}
// nodeId compatibility aliases
// define('foo/bar.js', ['foo/bar'], function(m) { return m; });
function nodeIdCompatAliases(moduleIds) {
let compat = {};
moduleIds.forEach(id => {
let ext = path.extname(id).toLowerCase();
if (!ext || Utils.knownExtensions.indexOf(ext) === -1) {
compat[id + '.js'] = id;
}
});
return compat;
}
@@ -91,6 +91,9 @@ exports.BundledSource = class {
}
let dependencyInclusion = this.dependencyInclusion;
let browserReplacement = dependencyInclusion &&
dependencyInclusion.description.browserReplacement();
let loaderPlugins = this._getLoaderPlugins();
let loaderConfig = this._getLoaderConfig();
let moduleId = this.moduleId;
@@ -133,7 +136,7 @@ exports.BundledSource = class {
}
}
deps = findDeps(modulePath, contents);
deps = [];
let context = {pkgsMainMap: {}, config: {shim: {}}};
let desc = dependencyInclusion && dependencyInclusion.description;
@@ -143,6 +146,7 @@ exports.BundledSource = class {
}
let wrapShim = false;
let replacement = {};
if (dependencyInclusion) {
let description = dependencyInclusion.description;
@@ -161,14 +165,42 @@ exports.BundledSource = class {
if (description.loaderConfig.wrapShim) {
wrapShim = true;
}
if (browserReplacement) {
for (let i = 0, keys = Object.keys(browserReplacement); i < keys.length; i++) {
let key = keys[i];
let target = browserReplacement[key];
const baseId = description.name + '/index';
const sourceModule = key.startsWith('.') ?
relativeModuleId(moduleId, absoluteModuleId(baseId, key)) :
key;
let targetModule;
if (target) {
targetModule = relativeModuleId(moduleId, absoluteModuleId(baseId, target));
} else {
// {"module-a": false}
// replace with special placeholder __ignore__
targetModule = '__ignore__';
}
replacement[sourceModule] = targetModule;
}
}
}
const writeTransform = allWriteTransforms({
stubModules: loaderConfig.stubModules,
wrapShim: wrapShim || loaderConfig.wrapShim
wrapShim: wrapShim || loaderConfig.wrapShim,
replacement: replacement
});
contents = writeTransform(context, moduleId, modulePath, contents);
const tracedDeps = findDeps(modulePath, contents);
if (tracedDeps && tracedDeps.length) {
deps.push.apply(deps, tracedDeps);
}
this.contents = contents;
}
@@ -182,7 +214,16 @@ exports.BundledSource = class {
// don't bother with local dependency in src,
// as we bundled all of local js/html/css files.
.filter(d => this.dependencyInclusion || d[0] !== '.')
.map(d => normalizeModuleId(moduleId, d));
.map(d => absoluteModuleId(moduleId, d))
.filter(d => {
// ignore false replacment
if (browserReplacement && browserReplacement.hasOwnProperty(d)) {
if (browserReplacement[d] === false) {
return false;
}
}
return true;
});
return moduleIds;
}
@@ -219,7 +260,7 @@ function stripPluginPrefixOrSubfix(moduleId) {
return moduleId;
}
function normalizeModuleId(baseId, moduleId) {
function absoluteModuleId(baseId, moduleId) {
if (moduleId[0] !== '.') return moduleId;
let parts = baseId.split('/');
@@ -237,6 +278,31 @@ function normalizeModuleId(baseId, moduleId) {
return parts.join('/');
}
function relativeModuleId(baseId, moduleId) {
if (moduleId[0] === '.') return moduleId;
let baseParts = baseId.split('/');
baseParts.pop();
let parts = moduleId.split('/');
while (parts.length && baseParts.length && baseParts[0] === parts[0]) {
baseParts.shift();
parts.shift();
}
let left = baseParts.length;
if (left === 0) {
parts.unshift('.');
} else {
for (let i = 0; i < left; i ++) {
parts.unshift('..');
}
}
return parts.join('/');
}
// if moduleId is above surface (default src/), the '../../' confuses hell out of
// requirejs as it tried to understand it as a relative module id.
// replace '..' with '__dot_dot__' to enforce absolute module id.
@@ -15,13 +15,7 @@ exports.DependencyDescription = class {
calculateMainPath(root) {
let config = this.loaderConfig;
let part;
if (config.main) {
part = path.join(config.path, config.main);
} else {
part = config.path;
}
let part = path.join(config.path, config.main);
let ext = path.extname(part).toLowerCase();
if (!ext || Utils.knownExtensions.indexOf(ext) === -1) {
@@ -41,4 +35,46 @@ exports.DependencyDescription = class {
return '';
}
}
// https://github.com/defunctzombie/package-browser-field-spec
browserReplacement() {
const browser = this.metadata && this.metadata.browser;
// string browser field is handled in package-analyzer
if (!browser || typeof browser === 'string') return;
let replacement = {};
for (let i = 0, keys = Object.keys(browser); i < keys.length; i++) {
let key = keys[i];
let target = browser[key];
let sourceModule = filePathToModuleId(key);
if (key.startsWith('.')) {
sourceModule = './' + sourceModule;
}
if (typeof target === 'string') {
let targetModule = filePathToModuleId(target);
if (!targetModule.startsWith('.')) {
targetModule = './' + targetModule;
}
replacement[sourceModule] = targetModule;
} else {
replacement[sourceModule] = false;
}
}
return replacement;
}
};
function filePathToModuleId(filePath) {
let moduleId = path.normalize(filePath).replace(/\\/g, '/');
if (moduleId.toLowerCase().endsWith('.js')) {
moduleId = moduleId.substr(0, moduleId.length - 3);
}
return moduleId;
}
View
@@ -8,6 +8,11 @@ const htmlparser = require('htmlparser2');
const path = require('path');
const fs = require('../file-system');
const amdNamedDefine = jsDepFinder(
'define(__dep, __any)',
'define(__dep, __any, __any)'
);
const auJsDepFinder = jsDepFinder(
'PLATFORM.moduleName(__dep)',
'__any.PLATFORM.moduleName(__dep)',
@@ -192,9 +197,9 @@ function _add(deps) {
// strip off leading /
if (clean[0] === '/') clean = clean.substr(1);
// There is some node module call themself like "popper.js",
// There is some npm package call itself like "popper.js",
// cannot strip .js from it.
if (clean.indexOf('/') !== -1) {
if (!isPackageName(clean)) {
// strip off tailing .js
clean = clean.replace(/\.js$/ig, '');
}
@@ -203,6 +208,13 @@ function _add(deps) {
});
}
function isPackageName(id) {
if (id.startsWith('.')) return false;
const parts = id.split('/');
// package name, or scope package name
return parts.length === 1 || (parts.length === 2 && parts[0].startsWith('@'));
}
exports.findJsDeps = function(filename, contents) {
let deps = new Set();
let add = _add.bind(deps);
@@ -215,6 +227,9 @@ exports.findJsDeps = function(filename, contents) {
// clear commonjs wrapper deps
['require', 'exports', 'module'].forEach(d => deps.delete(d));
// remove inner defined modules
amdNamedDefine(parsed).forEach(d => deps.delete(d));
// aurelia dependencies PLATFORM.moduleName and some others
add(auJsDepFinder(parsed));
@@ -68,5 +68,12 @@ module.exports = function(moduleId, root) {
logger.warn(`No avaiable stub for core Node.js module "${moduleId}", stubbed with empty module`);
return EMPTY_MODULE;
}
// https://github.com/defunctzombie/package-browser-field-spec
// {"module-a": false}
// replace with special placeholder __ignore__
if (moduleId === '__ignore__') {
return EMPTY_MODULE;
}
};
Oops, something went wrong.

0 comments on commit 5bb81d4

Please sign in to comment.