mirror of https://github.com/status-im/metro.git
Delta Bundler: Initial implementation of the Delta Bundler
Reviewed By: jeanlauliac Differential Revision: D5760233 fbshipit-source-id: 5f829d48401889b1391719564119951a1cf3c792
This commit is contained in:
parent
8c8cfb364f
commit
52fcaf4a10
|
@ -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;
|
|
@ -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;
|
|
@ -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']),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
Loading…
Reference in New Issue