Skip to content

Commit

Permalink
feat(bundler): fully support package.json browser field
Browse files Browse the repository at this point in the history
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
3cp committed Sep 27, 2018
1 parent 1669a6f commit 5bb81d4
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 54 deletions.
3 changes: 2 additions & 1 deletion lib/build/amodro-trace/write/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
// requirejs optimizer.
var transforms = [
require('./stubs'),
require('./defines')
require('./defines'),
require('./replace')
];

/**
Expand Down
87 changes: 87 additions & 0 deletions lib/build/amodro-trace/write/replace.js
Original file line number Diff line number Diff line change
@@ -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('@'));
}
21 changes: 0 additions & 21 deletions lib/build/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
74 changes: 70 additions & 4 deletions lib/build/bundled-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -133,7 +136,7 @@ exports.BundledSource = class {
}
}

deps = findDeps(modulePath, contents);
deps = [];

let context = {pkgsMainMap: {}, config: {shim: {}}};
let desc = dependencyInclusion && dependencyInclusion.description;
Expand All @@ -143,6 +146,7 @@ exports.BundledSource = class {
}

let wrapShim = false;
let replacement = {};
if (dependencyInclusion) {
let description = dependencyInclusion.description;

Expand All @@ -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;
}

Expand All @@ -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;
}
Expand Down Expand Up @@ -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('/');
Expand All @@ -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.
Expand Down
50 changes: 43 additions & 7 deletions lib/build/dependency-description.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
19 changes: 17 additions & 2 deletions lib/build/find-deps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down Expand Up @@ -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, '');
}
Expand All @@ -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);
Expand All @@ -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));

Expand Down
7 changes: 7 additions & 0 deletions lib/build/stub-core-nodejs-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};

Loading

0 comments on commit 5bb81d4

Please sign in to comment.