[react-packager] Introduce bundle IDs and keep track of parent/child

Summary:
Since JS doesn't have the guarantee that once a bundle is loaded it will stay in memory (and this is something we actually don't want to enforce to keep memmory usage low), we need to keep track of parent/child relationships on the packager to pass it down to native. As part of this diff, we also introduced an ID for each bundle. The ID for a child bundle is shynthetized as the bundleID of the parent module + an index which gets incremented every time a new bundle is created. For instance given this tree:

       a,b
    c       f
  d   e       g

the ID for `d` will be `bundle.0.1.2`, the one for e will be `bundle.0.1.3` and the one for `g` will be `bundle.0.5.6`. This information will be useful to figure out which bundles need to be loaded when a `require.ensure` is re-written.
This commit is contained in:
Martín Bigio 2015-08-20 08:40:09 -07:00
parent 54a8fe9156
commit 06eb63b54d
3 changed files with 322 additions and 112 deletions

View File

@ -29,8 +29,15 @@ describe('BundlesLayout', () => {
});
}
function isPolyfill() {
return false;
}
function dep(path) {
return {path};
return {
path: path,
isPolyfill: isPolyfill,
};
}
pit('should bundle sync dependencies', () => {
@ -52,9 +59,11 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual([
[dep('/root/index.js'), dep('/root/a.js')],
])
expect(bundles).toEqual({
id: 'bundle.0',
modules: [dep('/root/index.js'), dep('/root/a.js')],
children: [],
})
);
});
@ -77,10 +86,15 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual([
[dep('/root/index.js')],
[dep('/root/a.js')],
])
expect(bundles).toEqual({
id: 'bundle.0',
modules: [dep('/root/index.js')],
children: [{
id:'bundle.0.1',
modules: [dep('/root/a.js')],
children: [],
}],
})
);
});
@ -108,11 +122,19 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual([
[dep('/root/index.js')],
[dep('/root/a.js')],
[dep('/root/b.js')],
])
expect(bundles).toEqual({
id: 'bundle.0',
modules: [dep('/root/index.js')],
children: [{
id: 'bundle.0.1',
modules: [dep('/root/a.js')],
children: [{
id: 'bundle.0.1.2',
modules: [dep('/root/b.js')],
children: [],
}],
}],
})
);
});
@ -140,10 +162,15 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual([
[dep('/root/index.js')],
[dep('/root/a.js'), dep('/root/b.js')],
])
expect(bundles).toEqual({
id: 'bundle.0',
modules: [dep('/root/index.js')],
children: [{
id: 'bundle.0.1',
modules: [dep('/root/a.js'), dep('/root/b.js')],
children: [],
}],
})
);
});
@ -171,10 +198,15 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(
bundles => expect(bundles).toEqual([
[dep('/root/index.js'), dep('/root/a.js')],
[dep('/root/b.js')],
])
bundles => expect(bundles).toEqual({
id: 'bundle.0',
modules: [dep('/root/index.js'), dep('/root/a.js')],
children: [{
id: 'bundle.0.1',
modules: [dep('/root/b.js')],
children: [],
}],
})
);
});
@ -184,7 +216,7 @@ describe('BundlesLayout', () => {
case '/root/index.js':
return Promise.resolve({
dependencies: [dep('/root/index.js'), dep('/root/a.js')],
asyncDependencies: [['/root/b.js']],
asyncDependencies: [['/root/b.js'], ['/root/c.js']],
});
case '/root/a.js':
return Promise.resolve({
@ -194,13 +226,18 @@ describe('BundlesLayout', () => {
case '/root/b.js':
return Promise.resolve({
dependencies: [dep('/root/b.js')],
asyncDependencies: [['/root/c.js']],
asyncDependencies: [['/root/d.js']],
});
case '/root/c.js':
return Promise.resolve({
dependencies: [dep('/root/c.js')],
asyncDependencies: [],
});
case '/root/d.js':
return Promise.resolve({
dependencies: [dep('/root/d.js')],
asyncDependencies: [],
});
default:
throw 'Undefined path: ' + path;
}
@ -208,10 +245,11 @@ describe('BundlesLayout', () => {
var layout = newBundlesLayout();
return layout.generateLayout(['/root/index.js']).then(() => {
expect(layout.getBundleIDForModule('/root/index.js')).toBe(0);
expect(layout.getBundleIDForModule('/root/a.js')).toBe(0);
expect(layout.getBundleIDForModule('/root/b.js')).toBe(1);
expect(layout.getBundleIDForModule('/root/c.js')).toBe(2);
expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0');
expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0');
expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.0.1');
expect(layout.getBundleIDForModule('/root/c.js')).toBe('bundle.0.2');
expect(layout.getBundleIDForModule('/root/d.js')).toBe('bundle.0.1.3');
});
});
});

View File

@ -78,15 +78,37 @@ describe('BundlesLayout', () => {
return new BundlesLayout({dependencyResolver: resolver});
}
function modulePaths(bundles) {
if (!bundles) {
return null;
function stripPolyfills(bundle) {
return Promise
.all([
Promise.all(
bundle.modules.map(module => module
.getName()
.then(name => [module, name])
),
),
Promise.all(
bundle.children.map(childModule => stripPolyfills(childModule)),
),
])
.then(([modules, children]) => {
modules = modules
.filter(([module, name]) => { // filter polyfills
for (let p of polyfills) {
if (name.indexOf(p) !== -1) {
return false;
}
}
return true;
})
.map(([module, name]) => module.path);
return bundles.map(
bundle => bundle.filter(module => !module.isPolyfill())
.map(module => module.path)
);
return {
id: bundle.id,
modules: modules,
children: children,
};
});
}
function setMockFilesystem(mockFs) {
@ -104,10 +126,12 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
modulePaths(bundles).then(paths =>
expect(paths).toEqual([
['/root/index.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [],
})
)
);
});
@ -128,9 +152,13 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js', '/root/a.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js', '/root/a.js'],
children: [],
})
)
);
});
@ -150,10 +178,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js'],
children: [],
}],
})
)
);
});
@ -178,11 +213,23 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js'],
['/root/b.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [
{
id: 'bundle.0.1',
modules: ['/root/a.js'],
children: [],
}, {
id: 'bundle.0.2',
modules: ['/root/b.js'],
children: [],
},
],
})
)
);
});
@ -206,10 +253,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js', '/root/b.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/b.js'],
children: [],
}],
})
)
);
});
@ -234,10 +288,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js', '/root/a.js'],
['/root/b.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js', '/root/a.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/b.js'],
children: [],
}],
})
)
);
});
@ -267,10 +328,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js', '/root/b.js', '/root/c.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/b.js', '/root/c.js'],
children: [],
}],
})
)
);
});
@ -301,11 +369,24 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js', '/root/c.js'],
['/root/b.js', '/root/c.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [
{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/c.js'],
children: [],
},
{
id: 'bundle.0.2',
modules: ['/root/b.js', '/root/c.js'],
children: [],
},
],
})
)
);
});
@ -335,11 +416,23 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js'],
['/root/b.js', '/root/c.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [
{
id: 'bundle.0.1',
modules: ['/root/a.js'],
children: [{
id: 'bundle.0.1.2',
modules: ['/root/b.js', '/root/c.js'],
children: [],
}],
},
],
})
)
);
});
@ -369,10 +462,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js', '/root/c.js', '/root/b.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/c.js', '/root/b.js'],
children: [],
}],
})
)
);
});
@ -394,10 +494,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js', '/root/img.png'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/img.png'],
children: [],
}],
})
)
);
});
@ -425,11 +532,24 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js', '/root/img.png'],
['/root/b.js', '/root/img.png'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [
{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/img.png'],
children: [],
},
{
id: 'bundle.0.2',
modules: ['/root/b.js', '/root/img.png'],
children: [],
},
],
})
)
);
});
@ -446,10 +566,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/img.png'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/img.png'],
children: [],
}],
})
)
);
});
@ -471,10 +598,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/a.js', '/root/img.png'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/img.png'],
children: [],
}],
})
)
);
});
@ -491,10 +625,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/img.png'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/img.png'],
children: [],
}],
})
)
);
});
@ -521,10 +662,17 @@ describe('BundlesLayout', () => {
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([
['/root/index.js'],
['/root/aPackage/client.js'],
])
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/aPackage/client.js'],
children: [],
}],
})
)
);
});
});

View File

@ -18,6 +18,8 @@ const validateOpts = declareOpts({
},
});
const BUNDLE_PREFIX = 'bundle';
/**
* Class that takes care of separating the graph of dependencies into
* separate bundles
@ -31,16 +33,23 @@ class BundlesLayout {
}
generateLayout(entryPaths, isDev) {
const bundles = [];
var pending = [entryPaths];
var currentBundleID = 0;
const rootBundle = {
id: BUNDLE_PREFIX + '.' + currentBundleID++,
modules: [],
children: [],
};
var pending = [{paths: entryPaths, bundle: rootBundle}];
return promiseWhile(
() => pending.length > 0,
() => bundles,
() => rootBundle,
() => {
const {paths, bundle} = pending.shift();
// pending sync dependencies we still need to explore for the current
// pending dependency
let pendingSyncDeps = pending.shift();
const pendingSyncDeps = paths;
// accum variable for sync dependencies of the current pending
// dependency we're processing
@ -51,22 +60,31 @@ class BundlesLayout {
() => {
const dependencies = _.values(syncDependencies);
if (dependencies.length > 0) {
bundles.push(dependencies);
bundle.modules = dependencies;
}
},
() => {
index => {
const pendingSyncDep = pendingSyncDeps.shift();
return this._resolver
.getDependencies(pendingSyncDep, {dev: isDev})
.then(deps => {
deps.dependencies.forEach(dep => {
if (dep.path !== pendingSyncDep && !dep.isPolyfill) {
if (dep.path !== pendingSyncDep && !dep.isPolyfill()) {
pendingSyncDeps.push(dep.path);
}
syncDependencies[dep.path] = dep;
this._moduleToBundle[dep.path] = bundles.length;
this._moduleToBundle[dep.path] = bundle.id;
});
deps.asyncDependencies.forEach(asyncDeps => {
const childBundle = {
id: bundle.id + '.' + currentBundleID++,
modules: [],
children: [],
};
bundle.children.push(childBundle);
pending.push({paths: asyncDeps, bundle: childBundle});
});
pending = pending.concat(deps.asyncDependencies);
});
},
);
@ -83,11 +101,17 @@ class BundlesLayout {
// Once it's not satisfied anymore, it returns what the results callback
// indicates
function promiseWhile(condition, result, body) {
return _promiseWhile(condition, result, body, 0);
}
function _promiseWhile(condition, result, body, index) {
if (!condition()) {
return Promise.resolve(result());
}
return body().then(() => promiseWhile(condition, result, body));
return body(index).then(() =>
_promiseWhile(condition, result, body, index + 1)
);
}
module.exports = BundlesLayout;