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