[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) { function dep(path) {
return {path}; return {
path: path,
isPolyfill: isPolyfill,
};
} }
pit('should bundle sync dependencies', () => { pit('should bundle sync dependencies', () => {
@ -52,9 +59,11 @@ describe('BundlesLayout', () => {
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual([ expect(bundles).toEqual({
[dep('/root/index.js'), dep('/root/a.js')], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual([ expect(bundles).toEqual({
[dep('/root/index.js')], id: 'bundle.0',
[dep('/root/a.js')], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual([ expect(bundles).toEqual({
[dep('/root/index.js')], id: 'bundle.0',
[dep('/root/a.js')], modules: [dep('/root/index.js')],
[dep('/root/b.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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual([ expect(bundles).toEqual({
[dep('/root/index.js')], id: 'bundle.0',
[dep('/root/a.js'), dep('/root/b.js')], 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( return newBundlesLayout().generateLayout(['/root/index.js']).then(
bundles => expect(bundles).toEqual([ bundles => expect(bundles).toEqual({
[dep('/root/index.js'), dep('/root/a.js')], id: 'bundle.0',
[dep('/root/b.js')], 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': case '/root/index.js':
return Promise.resolve({ return Promise.resolve({
dependencies: [dep('/root/index.js'), dep('/root/a.js')], dependencies: [dep('/root/index.js'), dep('/root/a.js')],
asyncDependencies: [['/root/b.js']], asyncDependencies: [['/root/b.js'], ['/root/c.js']],
}); });
case '/root/a.js': case '/root/a.js':
return Promise.resolve({ return Promise.resolve({
@ -194,13 +226,18 @@ describe('BundlesLayout', () => {
case '/root/b.js': case '/root/b.js':
return Promise.resolve({ return Promise.resolve({
dependencies: [dep('/root/b.js')], dependencies: [dep('/root/b.js')],
asyncDependencies: [['/root/c.js']], asyncDependencies: [['/root/d.js']],
}); });
case '/root/c.js': case '/root/c.js':
return Promise.resolve({ return Promise.resolve({
dependencies: [dep('/root/c.js')], dependencies: [dep('/root/c.js')],
asyncDependencies: [], asyncDependencies: [],
}); });
case '/root/d.js':
return Promise.resolve({
dependencies: [dep('/root/d.js')],
asyncDependencies: [],
});
default: default:
throw 'Undefined path: ' + path; throw 'Undefined path: ' + path;
} }
@ -208,10 +245,11 @@ describe('BundlesLayout', () => {
var layout = newBundlesLayout(); var layout = newBundlesLayout();
return layout.generateLayout(['/root/index.js']).then(() => { return layout.generateLayout(['/root/index.js']).then(() => {
expect(layout.getBundleIDForModule('/root/index.js')).toBe(0); expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0');
expect(layout.getBundleIDForModule('/root/a.js')).toBe(0); expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0');
expect(layout.getBundleIDForModule('/root/b.js')).toBe(1); expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.0.1');
expect(layout.getBundleIDForModule('/root/c.js')).toBe(2); 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}); return new BundlesLayout({dependencyResolver: resolver});
} }
function modulePaths(bundles) { function stripPolyfills(bundle) {
if (!bundles) { return Promise
return null; .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( return {
bundle => bundle.filter(module => !module.isPolyfill()) id: bundle.id,
.map(module => module.path) modules: modules,
); children: children,
};
});
} }
function setMockFilesystem(mockFs) { function setMockFilesystem(mockFs) {
@ -104,10 +126,12 @@ describe('BundlesLayout', () => {
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
modulePaths(bundles).then(paths => stripPolyfills(bundles).then(resolvedBundles =>
expect(paths).toEqual([ expect(resolvedBundles).toEqual({
['/root/index.js'], id: 'bundle.0',
]) modules: ['/root/index.js'],
children: [],
})
) )
); );
}); });
@ -128,9 +152,13 @@ describe('BundlesLayout', () => {
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js', '/root/a.js'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js'], id: 'bundle.0',
['/root/b.js'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js', '/root/b.js'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js', '/root/a.js'], expect(resolvedBundles).toEqual({
['/root/b.js'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js', '/root/b.js', '/root/c.js'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js', '/root/c.js'], id: 'bundle.0',
['/root/b.js', '/root/c.js'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js'], id: 'bundle.0',
['/root/b.js', '/root/c.js'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js', '/root/c.js', '/root/b.js'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js', '/root/img.png'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js', '/root/img.png'], id: 'bundle.0',
['/root/b.js', '/root/img.png'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/img.png'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/a.js', '/root/img.png'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/img.png'], 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 => return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(modulePaths(bundles)).toEqual([ stripPolyfills(bundles).then(resolvedBundles =>
['/root/index.js'], expect(resolvedBundles).toEqual({
['/root/aPackage/client.js'], 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 * Class that takes care of separating the graph of dependencies into
* separate bundles * separate bundles
@ -31,16 +33,23 @@ class BundlesLayout {
} }
generateLayout(entryPaths, isDev) { generateLayout(entryPaths, isDev) {
const bundles = []; var currentBundleID = 0;
var pending = [entryPaths]; const rootBundle = {
id: BUNDLE_PREFIX + '.' + currentBundleID++,
modules: [],
children: [],
};
var pending = [{paths: entryPaths, bundle: rootBundle}];
return promiseWhile( return promiseWhile(
() => pending.length > 0, () => pending.length > 0,
() => bundles, () => rootBundle,
() => { () => {
const {paths, bundle} = pending.shift();
// pending sync dependencies we still need to explore for the current // pending sync dependencies we still need to explore for the current
// pending dependency // pending dependency
let pendingSyncDeps = pending.shift(); const pendingSyncDeps = paths;
// accum variable for sync dependencies of the current pending // accum variable for sync dependencies of the current pending
// dependency we're processing // dependency we're processing
@ -51,22 +60,31 @@ class BundlesLayout {
() => { () => {
const dependencies = _.values(syncDependencies); const dependencies = _.values(syncDependencies);
if (dependencies.length > 0) { if (dependencies.length > 0) {
bundles.push(dependencies); bundle.modules = dependencies;
} }
}, },
() => { index => {
const pendingSyncDep = pendingSyncDeps.shift(); const pendingSyncDep = pendingSyncDeps.shift();
return this._resolver return this._resolver
.getDependencies(pendingSyncDep, {dev: isDev}) .getDependencies(pendingSyncDep, {dev: isDev})
.then(deps => { .then(deps => {
deps.dependencies.forEach(dep => { deps.dependencies.forEach(dep => {
if (dep.path !== pendingSyncDep && !dep.isPolyfill) { if (dep.path !== pendingSyncDep && !dep.isPolyfill()) {
pendingSyncDeps.push(dep.path); pendingSyncDeps.push(dep.path);
} }
syncDependencies[dep.path] = dep; 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 // Once it's not satisfied anymore, it returns what the results callback
// indicates // indicates
function promiseWhile(condition, result, body) { function promiseWhile(condition, result, body) {
return _promiseWhile(condition, result, body, 0);
}
function _promiseWhile(condition, result, body, index) {
if (!condition()) { if (!condition()) {
return Promise.resolve(result()); return Promise.resolve(result());
} }
return body().then(() => promiseWhile(condition, result, body)); return body(index).then(() =>
_promiseWhile(condition, result, body, index + 1)
);
} }
module.exports = BundlesLayout; module.exports = BundlesLayout;