Permalink
Browse files

feat(bundler): support onRequiringModule(moduleId) callback

onRequiringModule callback is called before auto-tracing on a moduleId. It would not be called for any modules provided by app's src files or explicit dependencies config in aurelia.json.

Three types possible result (all can be returned in promise):
1. Boolean false: ignore this moduleId;
2. Array of strings like ['a', 'b']: require module id "a" and "b" instead;
3. A string: the full JavaScript content of this module
4. All other returns are ignored and go onto performing auto-tracing.
  • Loading branch information...
huochunpeng committed Aug 9, 2018
1 parent c4ce02c commit fd49eb176e98bfd56d8091c19891a46df846f722
View
@@ -9,6 +9,7 @@ const DependencyInclusion = require('./dependency-inclusion').DependencyInclusio
const Configuration = require('../configuration').Configuration;
const Utils = require('./utils');
const logger = require('aurelia-logging').getLogger('Bundle');
const knownExtensions = require('./known-extensions');
exports.Bundle = class {
constructor(bundler, config) {
@@ -482,8 +483,6 @@ function uniqueBy(collection, key) {
});
}
const knownExtensions = ['.js', '.json', '.css', '.svg', '.html'];
// nodeId compatibility aliases
// define('foo/bar.js', ['foo/bar'], function(m) { return m; });
function nodeIdCompatAliases(moduleIds) {
View
@@ -7,6 +7,7 @@ const Configuration = require('../configuration').Configuration;
const path = require('path');
const Utils = require('./utils');
const logger = require('aurelia-logging').getLogger('Bundler');
const knownExtensions = require('./known-extensions');
exports.Bundler = class {
constructor(project, packageAnalyzer, packageInstaller) {
@@ -131,9 +132,14 @@ exports.Bundler = class {
});
}
build() {
const doTranform = () => {
let deps = new Set();
build(opts) {
let onRequiringModule;
if (opts && typeof opts.onRequiringModule === 'function') {
onRequiringModule = opts.onRequiringModule;
}
const doTranform = (initSet) => {
let deps = new Set(initSet);
this.items.forEach(item => {
// Transformed items will be ignored
@@ -163,10 +169,52 @@ exports.Bundler = class {
}
if (deps.size) {
let _leftOver = new Set();
return Utils.runSequentially(
Array.from(deps).sort(),
d => this.addNpmResource(d)
).then(() => doTranform());
d => {
return new Promise(resolve => {
resolve(onRequiringModule && onRequiringModule(d));
}).then(
result => {
// ignore this module id
if (result === false) return;
// require other module ids instead
if (Array.isArray(result) && result.length) {
result.forEach(dd => _leftOver.add(dd));
return;
}
// got full content of this module
if (typeof result === 'string') {
let fakeFilePath = path.resolve(this.project.paths.root, d);
let ext = path.extname(d).toLowerCase();
if (!ext || Utils.knownExtensions.indexOf(ext) === -1) {
fakeFilePath += '.js';
}
// we use '/' as separator even on Windows
// because module id is using '/' as separator
this.addFile({
path: fakeFilePath,
contents: result
});
return;
}
// process normally if result is not recognizable
return this.addNpmResource(d);
},
// proceed normally after error
err => {
logger.error(err);
return this.addNpmResource(d);
}
);
}
).then(() => doTranform(_leftOver));
}
return Promise.resolve();
@@ -1,7 +1,7 @@
'use strict';
const path = require('path');
const fs = require('../file-system');
const knownExtensions = ['.js', '.css', '.svg', '.html'];
const knownExtensions = require('./known-extensions');
exports.DependencyDescription = class {
constructor(name, source) {
View
@@ -50,8 +50,8 @@ exports.bundle = function() {
});
};
exports.dest = function() {
return bundler.build()
exports.dest = function(opts) {
return bundler.build(opts)
.then(() => bundler.write());
};
@@ -0,0 +1,3 @@
'use strict';
module.exports = ['.js', '.json', '.css', '.svg', '.html'];
@@ -4,6 +4,7 @@ const path = require('path');
const DependencyDescription = require('./dependency-description').DependencyDescription;
const semver = require('semver');
const logger = require('aurelia-logging').getLogger('PackageAnalyzer');
const knownExtensions = require('./known-extensions');
exports.PackageAnalyzer = class {
constructor(project) {
@@ -119,8 +120,6 @@ function loadPackageMetadata(project, description) {
});
}
const knownExtensions = ['.js', '.json', '.css', '.svg', '.html'];
// loaderConfig.path is simplified when use didn't provide explicit config.
// In auto traced nodejs package, loaderConfig.path always matches description.location.
// We then use auto-generated moduleId aliases in dependency-inclusion to make AMD
@@ -1,5 +1,6 @@
'use strict';
const path = require('path');
const Bundler = require('../../../lib/build/bundler').Bundler;
const PackageAnalyzer = require('../../mocks/package-analyzer');
const CLIOptionsMock = require('../../mocks/cli-options');
@@ -620,6 +621,220 @@ describe('the Bundler module', () => {
.catch(e => done.fail(e));
});
it('build supports onRequiringModule to ignore module', done => {
let project = {
paths: {
root: 'src',
foo: 'bar'
},
build: { loader: {} }
};
let bundler = new Bundler(project, analyzer);
bundler.items = [
{
transform: jasmine.createSpy('transform1')
.and.returnValues(['f/bar', 'lorem', 'foo/lo'], undefined)
},
{
transform: jasmine.createSpy('transform2')
.and.returnValues(['foo', 'had'], undefined)
}
];
let bundle = {
getRawBundledModuleIds: () => ['had', 'f/bar/index'],
addAlias: jasmine.createSpy('addAlias')
};
bundler.bundles = [bundle];
bundler.addNpmResource = jasmine.createSpy('addNpmResource')
.and.returnValue(Promise.resolve());
bundler.build({
onRequiringModule: function(moduleId) {
if (moduleId === 'lorem') return false;
}
})
.then(() => {
expect(bundler.addNpmResource).toHaveBeenCalledTimes(2);
expect(bundler.addNpmResource.calls.argsFor(0)).toEqual(['foo']);
expect(bundler.addNpmResource.calls.argsFor(1)).toEqual(['foo/lo']);
expect(bundle.addAlias).toHaveBeenCalledTimes(1);
expect(bundle.addAlias).toHaveBeenCalledWith('f/bar', 'f/bar/index');
done();
})
.catch(e => done.fail(e));
});
it('build supports onRequiringModule to replace deps', done => {
let project = {
paths: {
root: 'src',
foo: 'bar'
},
build: { loader: {} }
};
let bundler = new Bundler(project, analyzer);
bundler.items = [
{
transform: jasmine.createSpy('transform1')
.and.returnValues(['f/bar', 'lorem', 'foo/lo'], undefined)
},
{
transform: jasmine.createSpy('transform2')
.and.returnValues(['foo', 'had'], undefined)
}
];
let bundle = {
getRawBundledModuleIds: () => ['had', 'f/bar/index'],
addAlias: jasmine.createSpy('addAlias')
};
bundler.bundles = [bundle];
bundler.addNpmResource = jasmine.createSpy('addNpmResource')
.and.returnValue(Promise.resolve());
bundler.build({
onRequiringModule: function(moduleId) {
if (moduleId === 'lorem') {
return new Promise(resolve => {
setTimeout(() => resolve(['lorem-a', 'lorem-b']), 50);
});
}
}
})
.then(() => {
expect(bundler.addNpmResource).toHaveBeenCalledTimes(4);
expect(bundler.addNpmResource.calls.argsFor(0)).toEqual(['foo']);
expect(bundler.addNpmResource.calls.argsFor(1)).toEqual(['foo/lo']);
expect(bundler.addNpmResource.calls.argsFor(2)).toEqual(['lorem-a']);
expect(bundler.addNpmResource.calls.argsFor(3)).toEqual(['lorem-b']);
expect(bundle.addAlias).toHaveBeenCalledTimes(1);
expect(bundle.addAlias).toHaveBeenCalledWith('f/bar', 'f/bar/index');
done();
})
.catch(e => done.fail(e));
});
it('build supports onRequiringModule to provide implementation', done => {
let project = {
paths: {
root: 'src',
foo: 'bar'
},
build: { loader: {} }
};
let bundler = new Bundler(project, analyzer);
bundler.items = [
{
transform: jasmine.createSpy('transform1')
.and.returnValues(['f/bar', 'lorem', 'foo/lo'], undefined)
},
{
transform: jasmine.createSpy('transform2')
.and.returnValues(['foo', 'had'], undefined)
}
];
let bundle = {
getRawBundledModuleIds: () => ['had', 'f/bar/index'],
addAlias: jasmine.createSpy('addAlias')
};
bundler.bundles = [bundle];
bundler.addFile = jasmine.createSpy('addFile').and.returnValue(null);
bundler.addNpmResource = jasmine.createSpy('addNpmResource')
.and.returnValue(Promise.resolve());
bundler.build({
onRequiringModule: function(moduleId) {
if (moduleId === 'lorem') return "define(['lorem-a', 'lorem-b'], function() {return 1;});";
}
})
.then(() => {
expect(bundler.addNpmResource).toHaveBeenCalledTimes(2);
expect(bundler.addNpmResource.calls.argsFor(0)).toEqual(['foo']);
expect(bundler.addNpmResource.calls.argsFor(1)).toEqual(['foo/lo']);
expect(bundler.addFile).toHaveBeenCalledTimes(1);
expect(bundler.addFile).toHaveBeenCalledWith({
path: path.resolve('src', 'lorem.js'),
contents: "define(['lorem-a', 'lorem-b'], function() {return 1;});"
});
expect(bundle.addAlias).toHaveBeenCalledTimes(1);
expect(bundle.addAlias).toHaveBeenCalledWith('f/bar', 'f/bar/index');
done();
})
.catch(e => done.fail(e));
});
it('build swallows onRequiringModule exception', done => {
let project = {
paths: {
root: 'src',
foo: 'bar'
},
build: { loader: {} }
};
let bundler = new Bundler(project, analyzer);
bundler.items = [
{
transform: jasmine.createSpy('transform1')
.and.returnValues(['f/bar', 'lorem', 'foo/lo'], undefined)
},
{
transform: jasmine.createSpy('transform2')
.and.returnValues(['foo', 'had'], undefined)
}
];
let bundle = {
getRawBundledModuleIds: () => ['had', 'f/bar/index'],
addAlias: jasmine.createSpy('addAlias')
};
bundler.bundles = [bundle];
bundler.addNpmResource = jasmine.createSpy('addNpmResource')
.and.returnValue(Promise.resolve());
bundler.build({
onRequiringModule: function(moduleId) {
if (moduleId === 'lorem') {
throw new Error('panic!');
}
}
})
.then(() => {
expect(bundler.addNpmResource).toHaveBeenCalledTimes(3);
expect(bundler.addNpmResource.calls.argsFor(0)).toEqual(['foo']);
expect(bundler.addNpmResource.calls.argsFor(1)).toEqual(['foo/lo']);
expect(bundler.addNpmResource.calls.argsFor(2)).toEqual(['lorem']);
expect(bundle.addAlias).toHaveBeenCalledTimes(1);
expect(bundle.addAlias).toHaveBeenCalledWith('f/bar', 'f/bar/index');
done();
})
.catch(e => done.fail(e));
});
afterEach(() => {
cliOptionsMock.detach();
});

0 comments on commit fd49eb1

Please sign in to comment.