Delta Bundler: Initial implementation of the Delta Bundler

Reviewed By: jeanlauliac

Differential Revision: D5760233

fbshipit-source-id: 5f829d48401889b1391719564119951a1cf3c792
This commit is contained in:
Rafael Oleza 2017-09-04 13:42:38 -07:00 committed by Facebook Github Bot
parent 8c8cfb364f
commit 52fcaf4a10
4 changed files with 979 additions and 0 deletions

View File

@ -0,0 +1,384 @@
/**
* 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.
*
* @flow
* @format
*/
'use strict';
import type Bundler, {BundlingOptions} from '../Bundler';
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
import type Resolver from '../Resolver';
import type {BundleOptions} from '../Server';
import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
import type Module from '../node-haste/Module';
export type DeltaResult = {
modified: Map<string, Module>,
deleted: Set<string>,
reset?: boolean,
};
/**
* This class is in charge of calculating the delta of changed modules that
* happen between calls. To do so, it subscribes to file changes, so it can
* traverse the files that have been changed between calls and avoid having to
* traverse the whole dependency tree for trivial small changes.
*/
class DeltaCalculator {
_bundler: Bundler;
_resolver: Resolver;
_options: BundleOptions;
_dependencies: Set<string> = new Set();
_shallowDependencies: Map<string, Set<string>> = new Map();
_modifiedFiles: Set<string> = new Set();
_currentBuildPromise: ?Promise<DeltaResult>;
_dependencyPairs: Map<string, $ReadOnlyArray<[string, Module]>> = new Map();
_modulesByName: Map<string, Module> = new Map();
_lastBundlingOptions: ?BundlingOptions;
_inverseDependencies: Map<string, Set<string>> = new Map();
constructor(bundler: Bundler, resolver: Resolver, options: BundleOptions) {
this._bundler = bundler;
this._options = options;
this._resolver = resolver;
this._resolver
.getDependencyGraph()
.getWatcher()
.on('change', this._handleMultipleFileChanges);
}
static async create(
bundler: Bundler,
options: BundleOptions,
): Promise<DeltaCalculator> {
const resolver = await bundler.getResolver();
return new DeltaCalculator(bundler, resolver, options);
}
/**
* Stops listening for file changes and clears all the caches.
*/
end() {
this._resolver
.getDependencyGraph()
.getWatcher()
.removeListener('change', this._handleMultipleFileChanges);
// Clean up all the cache data structures to deallocate memory.
this._dependencies = new Set();
this._shallowDependencies = new Map();
this._modifiedFiles = new Set();
this._dependencyPairs = new Map();
this._modulesByName = new Map();
}
/**
* Main method to calculate the delta of modules. It returns a DeltaResult,
* which contain the modified/added modules and the removed modules.
*/
async getDelta(): Promise<DeltaResult> {
// If there is already a build in progress, wait until it finish to start
// processing a new one (delta server doesn't support concurrent builds).
if (this._currentBuildPromise) {
await this._currentBuildPromise;
}
// We don't want the modified files Set to be modified while building the
// bundle, so we isolate them by using the current instance for the bundling
// and creating a new instance for the file watcher.
const modifiedFiles = this._modifiedFiles;
this._modifiedFiles = new Set();
// Concurrent requests should reuse the same bundling process. To do so,
// this method stores the promise as an instance variable, and then it's
// removed after it gets resolved.
this._currentBuildPromise = this._getDelta(modifiedFiles);
let result;
try {
result = await this._currentBuildPromise;
} finally {
this._currentBuildPromise = null;
}
return result;
}
/**
* Returns the options options object that is used by ResoltionRequest to
* read all the modules. This can be used by external objects to read again
* any module very fast (since the options object instance will be the same).
*/
getTransformerOptions(): JSTransformerOptions {
if (!this._lastBundlingOptions) {
throw new Error('Calculate a bundle first');
}
return this._lastBundlingOptions.transformer;
}
/**
* Returns all the dependency pairs for each of the modules. Each dependency
* pair consists of a string which corresponds to the relative path used in
* the `require()` statement and the Module object for that dependency.
*/
getDependencyPairs(): Map<string, $ReadOnlyArray<[string, Module]>> {
return this._dependencyPairs;
}
/**
* Returns a map of module names to Module objects (module name being the
* result of calling `Module.getName()`).
*/
getModulesByName(): Map<string, Module> {
return this._modulesByName;
}
getInverseDependencies(): Map<string, Set<string>> {
return this._inverseDependencies;
}
_handleMultipleFileChanges = ({eventsQueue}) => {
eventsQueue.forEach(this._handleFileChange);
};
/**
* Handles a single file change. To avoid doing any work before it's needed,
* the listener only stores the modified file, which will then be used later
* when the delta needs to be calculated.
*/
_handleFileChange = ({
type,
filePath,
}: {
type: string,
filePath: string,
}): mixed => {
this._modifiedFiles.add(filePath);
// TODO: Check if path is in current dependencies. If so, send an updated
// bundle event.
};
async _getDelta(modifiedFiles: Set<string>): Promise<DeltaResult> {
// If we call getDelta() without being initialized, we need get all
// dependencies and return a reset delta.
if (this._dependencies.size === 0) {
const {added} = await this._calculateAllDependencies();
return {
modified: added,
deleted: new Set(),
reset: true,
};
}
// We don't care about modified files that are not depended in the bundle.
// If any of these files is required by an existing file, it will
// automatically be picked up when calculating all dependencies.
const modifiedArray = Array.from(modifiedFiles).filter(file =>
this._dependencies.has(file),
);
// No changes happened. Return empty delta.
if (modifiedArray.length === 0) {
return {modified: new Map(), deleted: new Set()};
}
// Build the modules from the files that have been modified.
const modified = new Map(
await Promise.all(
modifiedArray.map(async file => {
const module = await this._bundler.getModuleForPath(file);
return [file, module];
}),
),
);
const filesWithChangedDependencies = await Promise.all(
modifiedArray.map(this._hasChangedDependencies, this),
);
// If there is no file with changes in its dependencies, we can just
// return the modified modules without recalculating the dependencies.
if (!filesWithChangedDependencies.some(value => value)) {
return {modified, deleted: new Set()};
}
// Recalculate all dependencies and append the newly added files to the
// modified files.
const {added, deleted} = await this._calculateAllDependencies();
for (const [key, value] of added) {
modified.set(key, value);
}
return {
modified,
deleted,
};
}
async _hasChangedDependencies(file: string) {
const module = await this._bundler.getModuleForPath(file);
if (!this._dependencies.has(module.path)) {
return false;
}
const newDependencies = await this._getShallowDependencies(module);
const oldDependencies = this._shallowDependencies.get(module.path);
if (!oldDependencies) {
return false;
}
// Update the dependency and inverse dependency caches for this module.
this._shallowDependencies.set(module.path, newDependencies);
return areDifferent(oldDependencies, newDependencies);
}
async _calculateAllDependencies(): Promise<{
added: Map<string, Module>,
deleted: Set<string>,
}> {
const added = new Map();
const response = await this._getAllDependencies();
const currentDependencies = response.dependencies;
this._lastBundlingOptions = response.options;
currentDependencies.forEach(module => {
if (this._dependencies.has(module.path)) {
// It's not a new dependency, we don't need to do anything.
return;
}
const dependencyPairs = response.getResolvedDependencyPairs(module);
this._dependencies.add(module.path);
this._shallowDependencies.set(
module.path,
new Set(dependencyPairs.map(([name, module]) => name)),
);
this._dependencyPairs.set(module.path, dependencyPairs);
added.set(module.path, module);
});
const deleted = new Set();
// We know that some files have been removed only if the size of the current
// dependencies is different that the size of the old dependencies after
// adding the new files.
if (currentDependencies.length !== this._dependencies.size) {
const currentSet = new Set(currentDependencies.map(dep => dep.path));
this._dependencies.forEach(file => {
if (currentSet.has(file)) {
return;
}
this._dependencies.delete(file);
this._shallowDependencies.delete(file);
this._dependencyPairs.delete(file);
deleted.add(file);
});
}
// Last iteration through all dependencies to populate the modulesByName
// cache (we could get rid of this if the `runBeforeMainModule` option was
// an asbsolute path).
await Promise.all(
currentDependencies.map(async module => {
const name = await module.getName();
this._modulesByName.set(name, module);
}),
);
// Yet another iteration through all the dependencies. This one is to
// calculate the inverse dependencies. Right now we cannot do a faster
// iteration to only calculate this for changed files since
// `Bundler.getShallowDependencies()` return the relative name of the
// dependencies (this logic is very similar to the one in
// getInverseDependencies.js on the react-native repo).
//
// TODO: consider moving this calculation directly to
// `getInverseDependencies()`.
this._inverseDependencies = new Map();
currentDependencies.forEach(module => {
const dependencies = this._dependencyPairs.get(module.path) || [];
dependencies.forEach(([name, dependencyModule]) => {
let inverse = this._inverseDependencies.get(dependencyModule.path);
if (!inverse) {
inverse = new Set();
this._inverseDependencies.set(dependencyModule.path, inverse);
}
inverse.add(module.path);
});
});
return {
added,
deleted,
};
}
async _getShallowDependencies(module: Module): Promise<Set<string>> {
if (module.isAsset() || module.isJSON()) {
return new Set();
}
const dependencies = await this._bundler.getShallowDependencies({
...this._options,
entryFile: module.path,
rootEntryFile: this._options.entryFile,
generateSourceMaps: false,
bundlingOptions: this._lastBundlingOptions || undefined,
});
return new Set(dependencies);
}
async _getAllDependencies(): Promise<
ResolutionResponse<Module, BundlingOptions>,
> {
return await this._bundler.getDependencies({
...this._options,
rootEntryFile: this._options.entryFile,
generateSourceMaps: this._options.generateSourceMaps,
prependPolyfills: false,
});
}
}
function areDifferent<T>(first: Set<T>, second: Set<T>): boolean {
if (first.size !== second.size) {
return true;
}
for (const element of first) {
if (!second.has(element)) {
return true;
}
}
return false;
}
module.exports = DeltaCalculator;

View File

@ -0,0 +1,319 @@
/**
* 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.
*
* @flow
* @format
*/
'use strict';
const DeltaCalculator = require('./DeltaCalculator');
import type Bundler from '../Bundler';
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
import type Resolver from '../Resolver';
import type {BundleOptions} from '../Server';
import type {MappingsMap} from '../lib/SourceMap';
import type Module from '../node-haste/Module';
export type DeltaTransformResponse = {
+pre: ?string,
+post: ?string,
+delta: {[key: string]: ?string},
};
type Options = {|
+getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>,
+polyfillModuleNames: $ReadOnlyArray<string>,
|};
/**
* This class is in charge of creating the delta bundle with the actual
* transformed source code for each of the modified modules.
*
* The delta bundle format is the following:
*
* {
* pre: '...', // source code to be prepended before all the modules.
* post: '...', // source code to be appended after all the modules
* // (normally here lay the require() call for the starup).
* delta: {
* 27: '...', // transformed source code of a modified module.
* 56: null, // deleted module.
* },
* }
*/
class DeltaTransformer {
_bundler: Bundler;
_getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>;
_polyfillModuleNames: $ReadOnlyArray<string>;
_getModuleId: ({path: string}) => number;
_deltaCalculator: DeltaCalculator;
_bundleOptions: BundleOptions;
_currentBuildPromise: ?Promise<DeltaTransformResponse>;
constructor(
bundler: Bundler,
deltaCalculator: DeltaCalculator,
options: Options,
bundleOptions: BundleOptions,
) {
this._bundler = bundler;
this._deltaCalculator = deltaCalculator;
this._getPolyfills = options.getPolyfills;
this._polyfillModuleNames = options.polyfillModuleNames;
this._getModuleId = this._bundler.getGetModuleIdFn();
this._bundleOptions = bundleOptions;
}
static async create(
bundler: Bundler,
options: Options,
bundleOptions: BundleOptions,
): Promise<DeltaTransformer> {
const deltaCalculator = await DeltaCalculator.create(
bundler,
bundleOptions,
);
return new DeltaTransformer(
bundler,
deltaCalculator,
options,
bundleOptions,
);
}
/**
* Destroy the Delta Transformer and its calculator. This should be used to
* clean up memory and resources once this instance is not used anymore.
*/
end() {
return this._deltaCalculator.end();
}
/**
* Main method to calculate the bundle delta. It returns a DeltaResult,
* which contain the source code of the modified and added modules and the
* list of removed modules.
*/
async getDelta(): Promise<DeltaTransformResponse> {
// If there is already a build in progress, wait until it finish to start
// processing a new one (delta transformer doesn't support concurrent
// builds).
if (this._currentBuildPromise) {
await this._currentBuildPromise;
}
this._currentBuildPromise = this._getDelta();
let result;
try {
result = await this._currentBuildPromise;
} finally {
this._currentBuildPromise = null;
}
return result;
}
async _getDelta(): Promise<DeltaTransformResponse> {
// Calculate the delta of modules.
const {modified, deleted, reset} = await this._deltaCalculator.getDelta();
const transformerOptions = this._deltaCalculator.getTransformerOptions();
const dependencyPairs = this._deltaCalculator.getDependencyPairs();
const resolver = await this._bundler.getResolver();
// Get the transformed source code of each modified/added module.
const modifiedDelta = await this._transformModules(
modified,
resolver,
transformerOptions,
dependencyPairs,
);
const deletedDelta = Object.create(null);
deleted.forEach(id => {
deletedDelta[this._getModuleId({path: id})] = null;
});
// Return the source code that gets prepended to all the modules. This
// contains polyfills and startup code (like the require() implementation).
const prependSources = reset
? await this._getPrepend(transformerOptions, dependencyPairs)
: null;
// Return the source code that gets appended to all the modules. This
// contains the require() calls to startup the execution of the modules.
const appendSources = reset
? await this._getAppend(
dependencyPairs,
this._deltaCalculator.getModulesByName(),
)
: null;
return {
pre: prependSources,
post: appendSources,
delta: {...modifiedDelta, ...deletedDelta},
reset,
};
}
async _getPrepend(
transformOptions: JSTransformerOptions,
dependencyPairs: Map<string, $ReadOnlyArray<[string, Module]>>,
): Promise<string> {
const resolver = await this._bundler.getResolver();
// Get all the polyfills from the relevant option params (the
// `getPolyfills()` method and the `polyfillModuleNames` variable).
const polyfillModuleNames = this._getPolyfills({
platform: this._bundleOptions.platform,
}).concat(this._polyfillModuleNames);
// The module system dependencies are scripts that need to be included at
// the very beginning of the bundle (before any polyfill).
const moduleSystemDeps = resolver.getModuleSystemDependencies({
dev: this._bundleOptions.dev,
});
const modules = moduleSystemDeps.concat(
polyfillModuleNames.map((polyfillModuleName, idx) =>
resolver.getDependencyGraph().createPolyfill({
file: polyfillModuleName,
id: polyfillModuleName,
dependencies: [],
}),
),
);
const sources = await Promise.all(
modules.map(async module => {
const result = await this._transformModule(
module,
resolver,
transformOptions,
dependencyPairs,
);
return result[1];
}),
);
return sources.join('\n;');
}
async _getAppend(
dependencyPairs: Map<string, $ReadOnlyArray<[string, Module]>>,
modulesByName: Map<string, Module>,
): Promise<string> {
const resolver = await this._bundler.getResolver();
// Get the absolute path of the entry file, in order to be able to get the
// actual correspondant module (and its moduleId) to be able to add the
// correct require(); call at the very end of the bundle.
const absPath = resolver
.getDependencyGraph()
.getAbsolutePath(this._bundleOptions.entryFile);
const entryPointModule = await this._bundler.getModuleForPath(absPath);
// First, get the modules correspondant to all the module names defined in
// the `runBeforeMainModule` config variable. Then, append the entry point
// module so the last thing that gets required is the entry point.
const sources = this._bundleOptions.runBeforeMainModule
.map(name => modulesByName.get(name))
.concat(entryPointModule)
.filter(Boolean)
.map(this._getModuleId)
.map(moduleId => `;require(${JSON.stringify(moduleId)});`);
return sources.join('\n');
}
async _transformModules(
modules: Map<string, Module>,
resolver: Resolver,
transformOptions: JSTransformerOptions,
dependencyPairs: Map<string, $ReadOnlyArray<[string, Module]>>,
): Promise<{[key: string]: string}> {
const transformedModules = await Promise.all(
Array.from(modules.values()).map(module =>
this._transformModule(
module,
resolver,
transformOptions,
dependencyPairs,
),
),
);
const output = Object.create(null);
transformedModules.forEach(([id, source]) => {
output[id] = source;
});
return output;
}
async _transformModule(
module: Module,
resolver: Resolver,
transformOptions: JSTransformerOptions,
dependencyPairs: Map<string, $ReadOnlyArray<[string, Module]>>,
): Promise<[number, string]> {
const [name, metadata] = await Promise.all([
module.getName(),
this._getMetadata(module, transformOptions),
]);
const dependencyPairsForModule = dependencyPairs.get(module.path) || [];
const wrapped = await resolver.wrapModule({
module,
getModuleId: this._getModuleId,
dependencyPairs: dependencyPairsForModule,
dependencyOffsets: metadata.dependencyOffsets || [],
name,
code: metadata.code,
map: metadata.map,
minify: this._bundleOptions.minify,
dev: this._bundleOptions.dev,
});
return [this._getModuleId(module), wrapped.code];
}
async _getMetadata(
module: Module,
transformOptions: JSTransformerOptions,
): Promise<{
+code: string,
+dependencyOffsets: ?Array<number>,
+map?: ?MappingsMap,
}> {
if (module.isAsset()) {
const asset = await this._bundler.generateAssetObjAndCode(
module,
this._bundleOptions.assetPlugins,
this._bundleOptions.platform,
);
return {
code: asset.code,
dependencyOffsets: asset.meta.dependencyOffsets,
map: undefined,
};
}
return await module.read(transformOptions);
}
}
module.exports = DeltaTransformer;

View File

@ -0,0 +1,190 @@
/**
* 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.
*
* @emails oncall+javascript_tools
* @format
*/
'use strict';
jest.mock('../../Bundler');
const Bundler = require('../../Bundler');
const {EventEmitter} = require('events');
const DeltaCalculator = require('../DeltaCalculator');
describe('DeltaCalculator', () => {
const moduleFoo = createModule({path: '/foo', name: 'foo'});
const moduleBar = createModule({path: '/bar', name: 'bar'});
const moduleBaz = createModule({path: '/baz', name: 'baz'});
let deltaCalculator;
let fileWatcher;
let mockedDependencies;
let mockedDependencyTree;
const bundlerMock = new Bundler();
const options = {
assetPlugins: [],
dev: true,
entryFile: 'bundle.js',
entryModuleOnly: false,
excludeSource: false,
generateSourceMaps: false,
hot: true,
inlineSourceMap: true,
isolateModuleIDs: false,
minify: false,
platform: 'ios',
runBeforeMainModule: ['core'],
runModule: true,
sourceMapUrl: undefined,
unbundle: false,
};
function createModule({path, name, isAsset, isJSON}) {
return {
path,
async getName() {
return name;
},
isAsset() {
return !!isAsset;
},
isJSON() {
return !!isAsset;
},
};
}
beforeEach(async () => {
mockedDependencies = [moduleFoo, moduleBar, moduleBaz];
mockedDependencyTree = new Map([[moduleFoo, [moduleBar, moduleBaz]]]);
fileWatcher = new EventEmitter();
Bundler.prototype.getResolver.mockReturnValue(
Promise.resolve({
getDependencyGraph() {
return {
getWatcher() {
return fileWatcher;
},
};
},
}),
);
Bundler.prototype.getDependencies.mockImplementation(async () => {
return {
options: {},
dependencies: mockedDependencies,
getResolvedDependencyPairs(module) {
const deps = mockedDependencyTree.get(module);
return deps ? deps.map(dep => [dep.name, dep]) : [];
},
};
});
Bundler.prototype.getModuleForPath.mockImplementation(async path => {
return mockedDependencies.filter(dep => dep.path === path)[0];
});
Bundler.prototype.getShallowDependencies.mockImplementation(
async module => {
const deps = mockedDependencyTree.get(module);
return deps ? await Promise.all(deps.map(dep => dep.getName())) : [];
},
);
deltaCalculator = await DeltaCalculator.create(bundlerMock, options);
});
it('should start listening for file changes after being initialized', async () => {
expect(fileWatcher.listeners('change')).toHaveLength(1);
});
it('should stop listening for file changes after being destroyed', () => {
deltaCalculator.end();
expect(fileWatcher.listeners('change')).toHaveLength(0);
});
it('should calculate the initial bundle correctly', async () => {
const result = await deltaCalculator.getDelta();
expect(result).toEqual({
modified: new Map([
['/foo', moduleFoo],
['/bar', moduleBar],
['/baz', moduleBaz],
]),
deleted: new Set(),
reset: true,
});
});
it('should return an empty delta when there are no changes', async () => {
await deltaCalculator.getDelta();
expect(await deltaCalculator.getDelta()).toEqual({
modified: new Map(),
deleted: new Set(),
});
});
it('should calculate a delta after a simple modification', async () => {
// Get initial delta
await deltaCalculator.getDelta();
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
const result = await deltaCalculator.getDelta();
expect(result).toEqual({
modified: new Map([['/foo', moduleFoo]]),
deleted: new Set(),
});
});
it('should calculate a delta after removing a dependency', async () => {
// Get initial delta
await deltaCalculator.getDelta();
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
// Remove moduleBar
mockedDependencyTree.set(moduleFoo, [moduleBaz]);
mockedDependencies = [moduleFoo, moduleBaz];
const result = await deltaCalculator.getDelta();
expect(result).toEqual({
modified: new Map([['/foo', moduleFoo]]),
deleted: new Set(['/bar']),
});
});
it('should calculate a delta after adding/removing dependencies', async () => {
// Get initial delta
await deltaCalculator.getDelta();
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
// Add moduleQux
const moduleQux = createModule({path: '/qux', name: 'qux'});
mockedDependencyTree.set(moduleFoo, [moduleQux]);
mockedDependencies = [moduleFoo, moduleQux];
const result = await deltaCalculator.getDelta();
expect(result).toEqual({
modified: new Map([['/foo', moduleFoo], ['/qux', moduleQux]]),
deleted: new Set(['/bar', '/baz']),
});
});
});

View File

@ -0,0 +1,86 @@
/**
* 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.
*
* @flow
* @format
*/
'use strict';
const DeltaTransformer = require('./DeltaTransformer');
import type Bundler from '../Bundler';
import type {BundleOptions} from '../Server';
export type DeltaBundle = {
id: string,
pre: ?string,
post: ?string,
delta: {[key: string]: ?string},
};
type MainOptions = {|
getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>,
polyfillModuleNames: $ReadOnlyArray<string>,
|};
type Options = BundleOptions & {+deltaBundleId: ?string};
/**
* `DeltaBundler` uses the `DeltaTransformer` to build bundle deltas. This
* module handles all the transformer instances so it can support multiple
* concurrent clients requesting their own deltas. This is done through the
* `deltaBundleId` options param (which maps a client to a specific delta
* transformer).
*/
class DeltaBundler {
_bundler: Bundler;
_options: MainOptions;
_deltaTransformers: Map<string, DeltaTransformer> = new Map();
_currentId: number = 0;
constructor(bundler: Bundler, options: MainOptions) {
this._bundler = bundler;
this._options = options;
}
/**
* Main method to build a Delta Bundler
*/
async build(options: Options): Promise<DeltaBundle> {
let bundleId = options.deltaBundleId;
// If no bundle id is passed, generate a new one (which is going to be
// returned as part of the bundle, so the client can later ask for an actual
// delta).
if (!bundleId) {
bundleId = String(this._currentId++);
}
let deltaTransformer = this._deltaTransformers.get(bundleId);
if (!deltaTransformer) {
deltaTransformer = await DeltaTransformer.create(
this._bundler,
this._options,
options,
);
this._deltaTransformers.set(bundleId, deltaTransformer);
}
const response = await deltaTransformer.getDelta();
return {
...response,
id: bundleId,
};
}
}
module.exports = DeltaBundler;