mirror of https://github.com/status-im/metro.git
[react-packager] Cache BundlesLayout
This commit is contained in:
parent
42ee9b1ca6
commit
b617c43a03
|
@ -13,6 +13,7 @@ const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const Promise = require('promise');
|
const Promise = require('promise');
|
||||||
const ProgressBar = require('progress');
|
const ProgressBar = require('progress');
|
||||||
|
const BundlesLayout = require('../BundlesLayout');
|
||||||
const Cache = require('../Cache');
|
const Cache = require('../Cache');
|
||||||
const Transformer = require('../JSTransformer');
|
const Transformer = require('../JSTransformer');
|
||||||
const DependencyResolver = require('../DependencyResolver');
|
const DependencyResolver = require('../DependencyResolver');
|
||||||
|
@ -104,6 +105,13 @@ class Bundler {
|
||||||
cache: this._cache,
|
cache: this._cache,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._bundlesLayout = new BundlesLayout({
|
||||||
|
dependencyResolver: this._resolver,
|
||||||
|
resetCache: opts.resetCache,
|
||||||
|
cacheVersion: opts.cacheVersion,
|
||||||
|
projectRoots: opts.projectRoots,
|
||||||
|
});
|
||||||
|
|
||||||
this._transformer = new Transformer({
|
this._transformer = new Transformer({
|
||||||
projectRoots: opts.projectRoots,
|
projectRoots: opts.projectRoots,
|
||||||
blacklistRE: opts.blacklistRE,
|
blacklistRE: opts.blacklistRE,
|
||||||
|
@ -120,6 +128,10 @@ class Bundler {
|
||||||
return this._cache.end();
|
return this._cache.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLayout(main, isDev) {
|
||||||
|
return this._bundlesLayout.generateLayout(main, isDev);
|
||||||
|
}
|
||||||
|
|
||||||
bundle(main, runModule, sourceMapUrl, isDev, platform) {
|
bundle(main, runModule, sourceMapUrl, isDev, platform) {
|
||||||
const bundle = new Bundle(sourceMapUrl);
|
const bundle = new Bundle(sourceMapUrl);
|
||||||
const findEventId = Activity.startEvent('find dependencies');
|
const findEventId = Activity.startEvent('find dependencies');
|
||||||
|
|
|
@ -8,249 +8,320 @@
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
jest
|
jest.dontMock('../index');
|
||||||
.dontMock('../index');
|
jest.mock('fs');
|
||||||
|
|
||||||
const Promise = require('promise');
|
const Promise = require('promise');
|
||||||
|
|
||||||
describe('BundlesLayout', () => {
|
describe('BundlesLayout', () => {
|
||||||
var BundlesLayout;
|
let BundlesLayout;
|
||||||
var DependencyResolver;
|
let DependencyResolver;
|
||||||
|
let loadCacheSync;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
BundlesLayout = require('../index');
|
BundlesLayout = require('../index');
|
||||||
DependencyResolver = require('../../DependencyResolver');
|
DependencyResolver = require('../../DependencyResolver');
|
||||||
|
loadCacheSync = require('../../lib/loadCacheSync');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generate', () => {
|
function newBundlesLayout(options) {
|
||||||
function newBundlesLayout() {
|
return new BundlesLayout(Object.assign({
|
||||||
return new BundlesLayout({
|
projectRoots: ['/root'],
|
||||||
dependencyResolver: new DependencyResolver(),
|
dependencyResolver: new DependencyResolver(),
|
||||||
});
|
}, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('layout', () => {
|
||||||
function isPolyfill() {
|
function isPolyfill() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dep(path) {
|
describe('getLayout', () => {
|
||||||
return {
|
function dep(path) {
|
||||||
path: path,
|
return {
|
||||||
isPolyfill: isPolyfill,
|
path: path,
|
||||||
};
|
isPolyfill: isPolyfill,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pit('should bundle sync dependencies', () => {
|
pit('should bundle sync dependencies', () => {
|
||||||
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
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: [],
|
asyncDependencies: [],
|
||||||
});
|
});
|
||||||
case '/root/a.js':
|
case '/root/a.js':
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
dependencies: [dep('/root/a.js')],
|
dependencies: [dep('/root/a.js')],
|
||||||
asyncDependencies: [],
|
asyncDependencies: [],
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
throw 'Undefined path: ' + path;
|
throw 'Undefined path: ' + path;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newBundlesLayout({resetCache: true})
|
||||||
|
.getLayout('/root/index.js')
|
||||||
|
.then(bundles =>
|
||||||
|
expect(bundles).toEqual({
|
||||||
|
id: 'bundle.0',
|
||||||
|
modules: ['/root/index.js', '/root/a.js'],
|
||||||
|
children: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
pit('should separate async dependencies into different bundle', () => {
|
||||||
expect(bundles).toEqual({
|
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
||||||
id: 'bundle.0',
|
switch (path) {
|
||||||
modules: ['/root/index.js', '/root/a.js'],
|
case '/root/index.js':
|
||||||
children: [],
|
return Promise.resolve({
|
||||||
})
|
dependencies: [dep('/root/index.js')],
|
||||||
);
|
asyncDependencies: [['/root/a.js']],
|
||||||
});
|
});
|
||||||
|
case '/root/a.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/a.js')],
|
||||||
|
asyncDependencies: [],
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw 'Undefined path: ' + path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
pit('should separate async dependencies into different bundle', () => {
|
return newBundlesLayout({resetCache: true})
|
||||||
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
.getLayout('/root/index.js')
|
||||||
switch (path) {
|
.then(bundles =>
|
||||||
case '/root/index.js':
|
expect(bundles).toEqual({
|
||||||
return Promise.resolve({
|
id: 'bundle.0',
|
||||||
dependencies: [dep('/root/index.js')],
|
modules: ['/root/index.js'],
|
||||||
asyncDependencies: [['/root/a.js']],
|
|
||||||
});
|
|
||||||
case '/root/a.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/a.js')],
|
|
||||||
asyncDependencies: [],
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
throw 'Undefined path: ' + path;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
|
||||||
expect(bundles).toEqual({
|
|
||||||
id: 'bundle.0',
|
|
||||||
modules: ['/root/index.js'],
|
|
||||||
children: [{
|
|
||||||
id:'bundle.0.1',
|
|
||||||
modules: ['/root/a.js'],
|
|
||||||
children: [],
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
pit('separate async dependencies of async dependencies', () => {
|
|
||||||
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
|
||||||
switch (path) {
|
|
||||||
case '/root/index.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/index.js')],
|
|
||||||
asyncDependencies: [['/root/a.js']],
|
|
||||||
});
|
|
||||||
case '/root/a.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/a.js')],
|
|
||||||
asyncDependencies: [['/root/b.js']],
|
|
||||||
});
|
|
||||||
case '/root/b.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/b.js')],
|
|
||||||
asyncDependencies: [],
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
throw 'Undefined path: ' + path;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
|
||||||
expect(bundles).toEqual({
|
|
||||||
id: 'bundle.0',
|
|
||||||
modules: ['/root/index.js'],
|
|
||||||
children: [{
|
|
||||||
id: 'bundle.0.1',
|
|
||||||
modules: ['/root/a.js'],
|
|
||||||
children: [{
|
children: [{
|
||||||
id: 'bundle.0.1.2',
|
id:'bundle.0.1',
|
||||||
|
modules: ['/root/a.js'],
|
||||||
|
children: [],
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
pit('separate async dependencies of async dependencies', () => {
|
||||||
|
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
||||||
|
switch (path) {
|
||||||
|
case '/root/index.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/index.js')],
|
||||||
|
asyncDependencies: [['/root/a.js']],
|
||||||
|
});
|
||||||
|
case '/root/a.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/a.js')],
|
||||||
|
asyncDependencies: [['/root/b.js']],
|
||||||
|
});
|
||||||
|
case '/root/b.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/b.js')],
|
||||||
|
asyncDependencies: [],
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw 'Undefined path: ' + path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newBundlesLayout({resetCache: true})
|
||||||
|
.getLayout('/root/index.js')
|
||||||
|
.then(bundles =>
|
||||||
|
expect(bundles).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'],
|
||||||
|
children: [],
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
pit('separate bundle sync dependencies of async ones on same bundle', () => {
|
||||||
|
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
||||||
|
switch (path) {
|
||||||
|
case '/root/index.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/index.js')],
|
||||||
|
asyncDependencies: [['/root/a.js']],
|
||||||
|
});
|
||||||
|
case '/root/a.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/a.js'), dep('/root/b.js')],
|
||||||
|
asyncDependencies: [],
|
||||||
|
});
|
||||||
|
case '/root/b.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/b.js')],
|
||||||
|
asyncDependencies: [],
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw 'Undefined path: ' + path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newBundlesLayout({resetCache: true})
|
||||||
|
.getLayout('/root/index.js')
|
||||||
|
.then(bundles =>
|
||||||
|
expect(bundles).toEqual({
|
||||||
|
id: 'bundle.0',
|
||||||
|
modules: ['/root/index.js'],
|
||||||
|
children: [{
|
||||||
|
id: 'bundle.0.1',
|
||||||
|
modules: ['/root/a.js', '/root/b.js'],
|
||||||
|
children: [],
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
pit('separate cache in which bundle is each dependency', () => {
|
||||||
|
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
||||||
|
switch (path) {
|
||||||
|
case '/root/index.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/index.js'), dep('/root/a.js')],
|
||||||
|
asyncDependencies: [],
|
||||||
|
});
|
||||||
|
case '/root/a.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/a.js')],
|
||||||
|
asyncDependencies: [['/root/b.js']],
|
||||||
|
});
|
||||||
|
case '/root/b.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/b.js')],
|
||||||
|
asyncDependencies: [],
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw 'Undefined path: ' + path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newBundlesLayout({resetCache: true})
|
||||||
|
.getLayout('/root/index.js')
|
||||||
|
.then(bundles =>
|
||||||
|
expect(bundles).toEqual({
|
||||||
|
id: 'bundle.0',
|
||||||
|
modules: ['/root/index.js', '/root/a.js'],
|
||||||
|
children: [{
|
||||||
|
id: 'bundle.0.1',
|
||||||
modules: ['/root/b.js'],
|
modules: ['/root/b.js'],
|
||||||
children: [],
|
children: [],
|
||||||
}],
|
}],
|
||||||
}],
|
})
|
||||||
})
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
pit('separate bundle sync dependencies of async ones on same bundle', () => {
|
|
||||||
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
|
||||||
switch (path) {
|
|
||||||
case '/root/index.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/index.js')],
|
|
||||||
asyncDependencies: [['/root/a.js']],
|
|
||||||
});
|
|
||||||
case '/root/a.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/a.js'), dep('/root/b.js')],
|
|
||||||
asyncDependencies: [],
|
|
||||||
});
|
|
||||||
case '/root/b.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/b.js')],
|
|
||||||
asyncDependencies: [],
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
throw 'Undefined path: ' + path;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
pit('separate cache in which bundle is each dependency', () => {
|
||||||
expect(bundles).toEqual({
|
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
||||||
|
switch (path) {
|
||||||
|
case '/root/index.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/index.js'), dep('/root/a.js')],
|
||||||
|
asyncDependencies: [['/root/b.js'], ['/root/c.js']],
|
||||||
|
});
|
||||||
|
case '/root/a.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/a.js')],
|
||||||
|
asyncDependencies: [],
|
||||||
|
});
|
||||||
|
case '/root/b.js':
|
||||||
|
return Promise.resolve({
|
||||||
|
dependencies: [dep('/root/b.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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var layout = newBundlesLayout({resetCache: true});
|
||||||
|
return layout.getLayout('/root/index.js').then(() => {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cache', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
loadCacheSync.mockReturnValue({
|
||||||
|
'/root/index.js': {
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
modules: ['/root/index.js'],
|
modules: ['/root/index.js'],
|
||||||
children: [{
|
children: [{
|
||||||
id: 'bundle.0.1',
|
id: 'bundle.0.1',
|
||||||
modules: ['/root/a.js', '/root/b.js'],
|
modules: ['/root/a.js'],
|
||||||
children: [],
|
children: [],
|
||||||
}],
|
}],
|
||||||
})
|
},
|
||||||
);
|
'/root/b.js': {
|
||||||
|
id: 'bundle.2',
|
||||||
|
modules: ['/root/b.js'],
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
pit('separate cache in which bundle is each dependency', () => {
|
pit('should load layouts', () => {
|
||||||
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
const layout = newBundlesLayout({ resetCache: false });
|
||||||
switch (path) {
|
|
||||||
case '/root/index.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/index.js'), dep('/root/a.js')],
|
|
||||||
asyncDependencies: [],
|
|
||||||
});
|
|
||||||
case '/root/a.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/a.js')],
|
|
||||||
asyncDependencies: [['/root/b.js']],
|
|
||||||
});
|
|
||||||
case '/root/b.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/b.js')],
|
|
||||||
asyncDependencies: [],
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
throw 'Undefined path: ' + path;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(
|
return Promise
|
||||||
bundles => expect(bundles).toEqual({
|
.all([
|
||||||
id: 'bundle.0',
|
layout.getLayout('/root/index.js'),
|
||||||
modules: ['/root/index.js', '/root/a.js'],
|
layout.getLayout('/root/b.js'),
|
||||||
children: [{
|
])
|
||||||
id: 'bundle.0.1',
|
.then(([layoutIndex, layoutB]) => {
|
||||||
|
expect(layoutIndex).toEqual({
|
||||||
|
id: 'bundle.0',
|
||||||
|
modules: ['/root/index.js'],
|
||||||
|
children: [{
|
||||||
|
id: 'bundle.0.1',
|
||||||
|
modules: ['/root/a.js'],
|
||||||
|
children: [],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(layoutB).toEqual({
|
||||||
|
id: 'bundle.2',
|
||||||
modules: ['/root/b.js'],
|
modules: ['/root/b.js'],
|
||||||
children: [],
|
children: [],
|
||||||
}],
|
});
|
||||||
})
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
pit('separate cache in which bundle is each dependency', () => {
|
it('should load moduleToBundle map', () => {
|
||||||
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
|
const layout = newBundlesLayout({ resetCache: false });
|
||||||
switch (path) {
|
|
||||||
case '/root/index.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/index.js'), dep('/root/a.js')],
|
|
||||||
asyncDependencies: [['/root/b.js'], ['/root/c.js']],
|
|
||||||
});
|
|
||||||
case '/root/a.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/a.js')],
|
|
||||||
asyncDependencies: [],
|
|
||||||
});
|
|
||||||
case '/root/b.js':
|
|
||||||
return Promise.resolve({
|
|
||||||
dependencies: [dep('/root/b.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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var layout = newBundlesLayout();
|
expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0');
|
||||||
return layout.generateLayout(['/root/index.js']).then(() => {
|
expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0.1');
|
||||||
expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0');
|
expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.2');
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -75,7 +75,11 @@ describe('BundlesLayout', () => {
|
||||||
assetRoots: ['/root'],
|
assetRoots: ['/root'],
|
||||||
});
|
});
|
||||||
|
|
||||||
return new BundlesLayout({dependencyResolver: resolver});
|
return new BundlesLayout({
|
||||||
|
dependencyResolver: resolver,
|
||||||
|
resetCache: true,
|
||||||
|
projectRoots: ['/root', '/' + __dirname.split('/')[1]],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripPolyfills(bundle) {
|
function stripPolyfills(bundle) {
|
||||||
|
@ -114,7 +118,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -140,7 +144,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -166,7 +170,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -201,7 +205,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -242,7 +246,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -282,7 +286,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -323,7 +327,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -370,7 +374,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -408,7 +412,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -446,7 +450,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -480,7 +484,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -512,7 +516,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -539,7 +543,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
@ -576,7 +580,7 @@ describe('BundlesLayout', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
|
return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
|
||||||
stripPolyfills(bundles).then(resolvedBundles =>
|
stripPolyfills(bundles).then(resolvedBundles =>
|
||||||
expect(resolvedBundles).toEqual({
|
expect(resolvedBundles).toEqual({
|
||||||
id: 'bundle.0',
|
id: 'bundle.0',
|
||||||
|
|
|
@ -8,14 +8,33 @@
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const Activity = require('../Activity');
|
||||||
|
|
||||||
const _ = require('underscore');
|
const _ = require('underscore');
|
||||||
const declareOpts = require('../lib/declareOpts');
|
const declareOpts = require('../lib/declareOpts');
|
||||||
|
const fs = require('fs');
|
||||||
|
const getCacheFilePath = require('../lib/getCacheFilePath');
|
||||||
|
const loadCacheSync = require('../lib/loadCacheSync');
|
||||||
|
const version = require('../../../../package.json').version;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
const validateOpts = declareOpts({
|
const validateOpts = declareOpts({
|
||||||
dependencyResolver: {
|
dependencyResolver: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
resetCache: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
cacheVersion: {
|
||||||
|
type: 'string',
|
||||||
|
default: '1.0',
|
||||||
|
},
|
||||||
|
projectRoots: {
|
||||||
|
type: 'array',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const BUNDLE_PREFIX = 'bundle';
|
const BUNDLE_PREFIX = 'bundle';
|
||||||
|
@ -29,19 +48,37 @@ class BundlesLayout {
|
||||||
const opts = validateOpts(options);
|
const opts = validateOpts(options);
|
||||||
this._resolver = opts.dependencyResolver;
|
this._resolver = opts.dependencyResolver;
|
||||||
|
|
||||||
|
// Cache in which bundle is each module.
|
||||||
this._moduleToBundle = Object.create(null);
|
this._moduleToBundle = Object.create(null);
|
||||||
|
|
||||||
|
// Cache the bundles layouts for each entry point. This entries
|
||||||
|
// are not evicted unless the user explicitly specifies so as
|
||||||
|
// computing them is pretty expensive
|
||||||
|
this._layouts = Object.create(null);
|
||||||
|
|
||||||
|
// TODO: watch for file creations and removals to update this caches
|
||||||
|
|
||||||
|
this._cacheFilePath = this._getCacheFilePath(opts);
|
||||||
|
if (!opts.resetCache) {
|
||||||
|
this._loadCacheSync(this._cacheFilePath);
|
||||||
|
} else {
|
||||||
|
this._persistCacheEventually();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generateLayout(entryPaths, isDev) {
|
getLayout(entryPath, isDev) {
|
||||||
|
if (this._layouts[entryPath]) {
|
||||||
|
return this._layouts[entryPath];
|
||||||
|
}
|
||||||
var currentBundleID = 0;
|
var currentBundleID = 0;
|
||||||
const rootBundle = {
|
const rootBundle = {
|
||||||
id: BUNDLE_PREFIX + '.' + currentBundleID++,
|
id: BUNDLE_PREFIX + '.' + currentBundleID++,
|
||||||
modules: [],
|
modules: [],
|
||||||
children: [],
|
children: [],
|
||||||
};
|
};
|
||||||
var pending = [{paths: entryPaths, bundle: rootBundle}];
|
var pending = [{paths: [entryPath], bundle: rootBundle}];
|
||||||
|
|
||||||
return promiseWhile(
|
this._layouts[entryPath] = promiseWhile(
|
||||||
() => pending.length > 0,
|
() => pending.length > 0,
|
||||||
() => rootBundle,
|
() => rootBundle,
|
||||||
() => {
|
() => {
|
||||||
|
@ -62,6 +99,9 @@ class BundlesLayout {
|
||||||
if (dependencies.length > 0) {
|
if (dependencies.length > 0) {
|
||||||
bundle.modules = dependencies;
|
bundle.modules = dependencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// persist changes to layouts
|
||||||
|
this._persistCacheEventually();
|
||||||
},
|
},
|
||||||
index => {
|
index => {
|
||||||
const pendingSyncDep = pendingSyncDeps.shift();
|
const pendingSyncDep = pendingSyncDeps.shift();
|
||||||
|
@ -90,11 +130,71 @@ class BundlesLayout {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return this._layouts[entryPath];
|
||||||
}
|
}
|
||||||
|
|
||||||
getBundleIDForModule(path) {
|
getBundleIDForModule(path) {
|
||||||
return this._moduleToBundle[path];
|
return this._moduleToBundle[path];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_loadCacheSync(cachePath) {
|
||||||
|
const loadCacheId = Activity.startEvent('Loading bundles layout');
|
||||||
|
const cacheOnDisk = loadCacheSync(cachePath);
|
||||||
|
|
||||||
|
// TODO: create single-module bundles for unexistent modules
|
||||||
|
// TODO: remove modules that no longer exist
|
||||||
|
Object.keys(cacheOnDisk).forEach(entryPath => {
|
||||||
|
this._layouts[entryPath] = Promise.resolve(cacheOnDisk[entryPath]);
|
||||||
|
this._fillModuleToBundleMap(cacheOnDisk[entryPath]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Activity.endEvent(loadCacheId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fillModuleToBundleMap(bundle) {
|
||||||
|
bundle.modules.forEach(module => this._moduleToBundle[module] = bundle.id);
|
||||||
|
bundle.children.forEach(child => this._fillModuleToBundleMap(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistCacheEventually() {
|
||||||
|
_.debounce(
|
||||||
|
this._persistCache.bind(this),
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistCache() {
|
||||||
|
if (this._persisting !== null) {
|
||||||
|
return this._persisting;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._persisting = Promise
|
||||||
|
.all(_.values(this._layouts))
|
||||||
|
.then(bundlesLayout => {
|
||||||
|
var json = Object.create(null);
|
||||||
|
Object.keys(this._layouts).forEach((p, i) =>
|
||||||
|
json[p] = bundlesLayout[i]
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.denodeify(fs.writeFile)(
|
||||||
|
this._cacheFilepath,
|
||||||
|
JSON.stringify(json),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => this._persisting = null);
|
||||||
|
|
||||||
|
return this._persisting;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCacheFilePath(options) {
|
||||||
|
return getCacheFilePath(
|
||||||
|
'react-packager-bundles-cache-',
|
||||||
|
version,
|
||||||
|
options.projectRoots.join(',').split(path.sep).join('-'),
|
||||||
|
options.cacheVersion || '0',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs the body Promise meanwhile the condition callback is satisfied.
|
// Runs the body Promise meanwhile the condition callback is satisfied.
|
||||||
|
|
|
@ -11,7 +11,9 @@
|
||||||
jest
|
jest
|
||||||
.dontMock('underscore')
|
.dontMock('underscore')
|
||||||
.dontMock('absolute-path')
|
.dontMock('absolute-path')
|
||||||
.dontMock('../');
|
.dontMock('../')
|
||||||
|
.dontMock('../../lib/loadCacheSync')
|
||||||
|
.dontMock('../../lib/getCacheFilePath');
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.mock('os')
|
.mock('os')
|
||||||
|
|
|
@ -8,17 +8,17 @@
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var _ = require('underscore');
|
const Promise = require('promise');
|
||||||
var crypto = require('crypto');
|
const _ = require('underscore');
|
||||||
var declareOpts = require('../lib/declareOpts');
|
const declareOpts = require('../lib/declareOpts');
|
||||||
var fs = require('fs');
|
const fs = require('fs');
|
||||||
var isAbsolutePath = require('absolute-path');
|
const getCacheFilePath = require('../lib/getCacheFilePath');
|
||||||
var path = require('path');
|
const isAbsolutePath = require('absolute-path');
|
||||||
var Promise = require('promise');
|
const loadCacheSync = require('../lib/loadCacheSync');
|
||||||
var tmpdir = require('os').tmpDir();
|
const path = require('path');
|
||||||
var version = require('../../../../package.json').version;
|
const version = require('../../../../package.json').version;
|
||||||
|
|
||||||
var validateOpts = declareOpts({
|
const validateOpts = declareOpts({
|
||||||
resetCache: {
|
resetCache: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -164,21 +164,7 @@ class Cache {
|
||||||
|
|
||||||
_loadCacheSync(cachePath) {
|
_loadCacheSync(cachePath) {
|
||||||
var ret = Object.create(null);
|
var ret = Object.create(null);
|
||||||
if (!fs.existsSync(cachePath)) {
|
var cacheOnDisk = loadCacheSync(cachePath);
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cacheOnDisk;
|
|
||||||
try {
|
|
||||||
cacheOnDisk = JSON.parse(fs.readFileSync(cachePath));
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof SyntaxError) {
|
|
||||||
console.warn('Unable to parse cache file. Will clear and continue.');
|
|
||||||
fs.unlinkSync(cachePath);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter outdated cache and convert to promises.
|
// Filter outdated cache and convert to promises.
|
||||||
Object.keys(cacheOnDisk).forEach(key => {
|
Object.keys(cacheOnDisk).forEach(key => {
|
||||||
|
@ -203,20 +189,13 @@ class Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
_getCacheFilePath(options) {
|
_getCacheFilePath(options) {
|
||||||
var hash = crypto.createHash('md5');
|
return getCacheFilePath(
|
||||||
hash.update(version);
|
'react-packager-cache-',
|
||||||
|
version,
|
||||||
var roots = options.projectRoots.join(',').split(path.sep).join('-');
|
options.projectRoots.join(',').split(path.sep).join('-'),
|
||||||
hash.update(roots);
|
options.cacheVersion || '0',
|
||||||
|
options.transformModulePath,
|
||||||
var cacheVersion = options.cacheVersion || '0';
|
);
|
||||||
hash.update(cacheVersion);
|
|
||||||
|
|
||||||
hash.update(options.transformModulePath);
|
|
||||||
|
|
||||||
var name = 'react-packager-cache-' + hash.digest('hex');
|
|
||||||
|
|
||||||
return path.join(tmpdir, name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const path = require('path');
|
||||||
|
const tmpdir = require('os').tmpDir();
|
||||||
|
|
||||||
|
function getCacheFilePath(args) {
|
||||||
|
args = Array.prototype.slice.call(args);
|
||||||
|
const prefix = args.shift();
|
||||||
|
|
||||||
|
let hash = crypto.createHash('md5');
|
||||||
|
args.forEach(arg => hash.update(arg));
|
||||||
|
|
||||||
|
return path.join(tmpdir, prefix + hash.digest('hex'));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getCacheFilePath;
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
function loadCacheSync(cachePath) {
|
||||||
|
if (!fs.existsSync(cachePath)) {
|
||||||
|
return Object.create(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(cachePath));
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof SyntaxError) {
|
||||||
|
console.warn('Unable to parse cache file. Will clear and continue.');
|
||||||
|
fs.unlinkSync(cachePath);
|
||||||
|
return Object.create(null);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = loadCacheSync;
|
Loading…
Reference in New Issue