Skip to content

Commit

Permalink
feat(bundler): support onRequiringModule(moduleId) callback
Browse files Browse the repository at this point in the history
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
3cp committed Sep 27, 2018
1 parent c4ce02c commit fd49eb1
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 12 deletions.
3 changes: 1 addition & 2 deletions lib/build/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
58 changes: 53 additions & 5 deletions lib/build/bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion lib/build/dependency-description.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions lib/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ exports.bundle = function() {
});
};

exports.dest = function() {
return bundler.build()
exports.dest = function(opts) {
return bundler.build(opts)
.then(() => bundler.write());
};

Expand Down
3 changes: 3 additions & 0 deletions lib/build/known-extensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = ['.js', '.json', '.css', '.svg', '.html'];
3 changes: 1 addition & 2 deletions lib/build/package-analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
215 changes: 215 additions & 0 deletions spec/lib/build/bundler.spec.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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();
});
Expand Down

0 comments on commit fd49eb1

Please sign in to comment.