mirror of
https://github.com/status-im/metro.git
synced 2025-02-17 21:46:24 +00:00
Integrate the incremental dependency traversor into the Delta Bundler
Reviewed By: davidaurelio Differential Revision: D5880892 fbshipit-source-id: d6e04b59aee5fe0bf671553c98f728be054e01b9
This commit is contained in:
parent
7e65f2f1ea
commit
a606e79af0
@ -12,13 +12,16 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
initialTraverseDependencies,
|
||||
traverseDependencies,
|
||||
} = require('./traverseDependencies');
|
||||
const {EventEmitter} = require('events');
|
||||
|
||||
import type Bundler, {BundlingOptions} from '../Bundler';
|
||||
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 ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
|
||||
import type DependencyGraph from '../node-haste/DependencyGraph';
|
||||
import type Module from '../node-haste/Module';
|
||||
|
||||
export type DeltaResult = {|
|
||||
@ -27,9 +30,7 @@ export type DeltaResult = {|
|
||||
+reset: boolean,
|
||||
|};
|
||||
|
||||
export type ShallowDependencies = Map<string, Map<string, string>>;
|
||||
export type ModulePaths = Set<string>;
|
||||
export type InverseDependencies = Map<string, Set<string>>;
|
||||
import type {DependencyEdges} from './traverseDependencies';
|
||||
|
||||
/**
|
||||
* This class is in charge of calculating the delta of changed modules that
|
||||
@ -39,27 +40,28 @@ export type InverseDependencies = Map<string, Set<string>>;
|
||||
*/
|
||||
class DeltaCalculator extends EventEmitter {
|
||||
_bundler: Bundler;
|
||||
_resolver: Resolver;
|
||||
_dependencyGraph: DependencyGraph;
|
||||
_options: BundleOptions;
|
||||
_transformerOptions: ?JSTransformerOptions;
|
||||
|
||||
_currentBuildPromise: ?Promise<DeltaResult>;
|
||||
_deletedFiles: Set<string> = new Set();
|
||||
_modifiedFiles: Set<string> = new Set();
|
||||
|
||||
_modules: ModulePaths = new Set();
|
||||
_shallowDependencies: ShallowDependencies = new Map();
|
||||
_modulesByName: Map<string, Module> = new Map();
|
||||
_inverseDependencies: InverseDependencies = new Map();
|
||||
_dependencyEdges: DependencyEdges = new Map();
|
||||
|
||||
constructor(bundler: Bundler, resolver: Resolver, options: BundleOptions) {
|
||||
constructor(
|
||||
bundler: Bundler,
|
||||
dependencyGraph: DependencyGraph,
|
||||
options: BundleOptions,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._bundler = bundler;
|
||||
this._options = options;
|
||||
this._resolver = resolver;
|
||||
this._dependencyGraph = dependencyGraph;
|
||||
|
||||
this._resolver
|
||||
.getDependencyGraph()
|
||||
this._dependencyGraph
|
||||
.getWatcher()
|
||||
.on('change', this._handleMultipleFileChanges);
|
||||
}
|
||||
@ -68,16 +70,14 @@ class DeltaCalculator extends EventEmitter {
|
||||
* Stops listening for file changes and clears all the caches.
|
||||
*/
|
||||
end() {
|
||||
this._resolver
|
||||
.getDependencyGraph()
|
||||
this._dependencyGraph
|
||||
.getWatcher()
|
||||
.removeListener('change', this._handleMultipleFileChanges);
|
||||
|
||||
// Clean up all the cache data structures to deallocate memory.
|
||||
this._modules = new Set();
|
||||
this._shallowDependencies = new Map();
|
||||
this._modifiedFiles = new Set();
|
||||
this._modulesByName = new Map();
|
||||
this._deletedFiles = new Set();
|
||||
this._dependencyEdges = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,14 +96,21 @@ class DeltaCalculator extends EventEmitter {
|
||||
// and creating a new instance for the file watcher.
|
||||
const modifiedFiles = this._modifiedFiles;
|
||||
this._modifiedFiles = new Set();
|
||||
const deletedFiles = this._deletedFiles;
|
||||
this._deletedFiles = 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);
|
||||
this._currentBuildPromise = this._getChangedDependencies(
|
||||
modifiedFiles,
|
||||
deletedFiles,
|
||||
);
|
||||
|
||||
let result;
|
||||
|
||||
const numDependencies = this._dependencyEdges.size;
|
||||
|
||||
try {
|
||||
result = await this._currentBuildPromise;
|
||||
} catch (error) {
|
||||
@ -112,6 +119,14 @@ class DeltaCalculator extends EventEmitter {
|
||||
// do so, asking for a delta after an error will produce an empty Delta,
|
||||
// which is not correct.
|
||||
modifiedFiles.forEach(file => this._modifiedFiles.add(file));
|
||||
deletedFiles.forEach(file => this._deletedFiles.add(file));
|
||||
|
||||
// If after an error the number of edges has changed, we could be in
|
||||
// a weird state. As a safe net we clean the dependency edges to force
|
||||
// a clean traversal of the graph next time.
|
||||
if (this._dependencyEdges.size !== numDependencies) {
|
||||
this._dependencyEdges = new Map();
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
@ -126,24 +141,30 @@ class DeltaCalculator extends EventEmitter {
|
||||
* 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 {
|
||||
async getTransformerOptions(): Promise<JSTransformerOptions> {
|
||||
if (!this._transformerOptions) {
|
||||
throw new Error('Calculate a bundle first');
|
||||
this._transformerOptions = (await this._bundler.getTransformOptions(
|
||||
this._options.entryFile,
|
||||
{
|
||||
dev: this._options.dev,
|
||||
generateSourceMaps: false,
|
||||
hot: this._options.hot,
|
||||
minify: this._options.minify,
|
||||
platform: this._options.platform,
|
||||
prependPolyfills: false,
|
||||
},
|
||||
)).transformer;
|
||||
}
|
||||
return this._transformerOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns all the dependency edges from the graph. Each edge contains the
|
||||
* needed information to do the traversing (dependencies, inverseDependencies)
|
||||
* plus some metadata.
|
||||
*/
|
||||
getShallowDependencies(): ShallowDependencies {
|
||||
return this._shallowDependencies;
|
||||
}
|
||||
|
||||
getInverseDependencies(): Map<string, Set<string>> {
|
||||
return this._inverseDependencies;
|
||||
getDependencyEdges(): DependencyEdges {
|
||||
return this._dependencyEdges;
|
||||
}
|
||||
|
||||
_handleMultipleFileChanges = ({eventsQueue}) => {
|
||||
@ -162,75 +183,87 @@ class DeltaCalculator extends EventEmitter {
|
||||
type: string,
|
||||
filePath: string,
|
||||
}): mixed => {
|
||||
// We do not want to keep track of deleted files, since this can cause
|
||||
// issues when moving files (or even deleting files).
|
||||
// The only issue with this approach is that the user removes a file that
|
||||
// is needed, the bundler will still create a correct bundle (since it
|
||||
// won't detect any modified file). Once we have our own dependency
|
||||
// traverser in Delta Bundler this will be easy to fix.
|
||||
if (type === 'delete') {
|
||||
this._modules.delete(filePath);
|
||||
return;
|
||||
this._deletedFiles.add(filePath);
|
||||
} else {
|
||||
this._modifiedFiles.add(filePath);
|
||||
}
|
||||
|
||||
this._modifiedFiles.add(filePath);
|
||||
|
||||
// Notify users that there is a change in some of the bundle files. This
|
||||
// way the client can choose to refetch the bundle.
|
||||
if (this._modules.has(filePath)) {
|
||||
this.emit('change');
|
||||
}
|
||||
this.emit('change');
|
||||
};
|
||||
|
||||
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._modules.size === 0) {
|
||||
const {added} = await this._calculateAllDependencies();
|
||||
async _getChangedDependencies(
|
||||
modifiedFiles: Set<string>,
|
||||
deletedFiles: Set<string>,
|
||||
): Promise<DeltaResult> {
|
||||
const transformerOptions = await this.getTransformerOptions();
|
||||
|
||||
if (!this._dependencyEdges.size) {
|
||||
const path = this._dependencyGraph.getAbsolutePath(
|
||||
this._options.entryFile,
|
||||
);
|
||||
|
||||
const modified = new Map([
|
||||
[path, this._dependencyGraph.getModuleForPath(path)],
|
||||
]);
|
||||
|
||||
const {added} = await initialTraverseDependencies(
|
||||
path,
|
||||
this._dependencyGraph,
|
||||
transformerOptions,
|
||||
this._dependencyEdges,
|
||||
this._options.onProgress || undefined,
|
||||
);
|
||||
|
||||
for (const path of added) {
|
||||
modified.set(path, this._dependencyGraph.getModuleForPath(path));
|
||||
}
|
||||
|
||||
return {
|
||||
modified: added,
|
||||
modified,
|
||||
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._modules.has(file),
|
||||
// If a file has been deleted, we want to invalidate any other file that
|
||||
// depends on it, so we can process it and correctly return an error.
|
||||
deletedFiles.forEach(filePath => {
|
||||
const edge = this._dependencyEdges.get(filePath);
|
||||
|
||||
if (edge) {
|
||||
edge.inverseDependencies.forEach(path => modifiedFiles.add(path));
|
||||
}
|
||||
});
|
||||
|
||||
// We only want to process files that are in the bundle.
|
||||
const modifiedDependencies = Array.from(modifiedFiles).filter(filePath =>
|
||||
this._dependencyEdges.has(filePath),
|
||||
);
|
||||
|
||||
// No changes happened. Return empty delta.
|
||||
if (modifiedArray.length === 0) {
|
||||
if (modifiedDependencies.length === 0) {
|
||||
return {modified: new Map(), deleted: new Set(), reset: false};
|
||||
}
|
||||
|
||||
// Build the modules from the files that have been modified.
|
||||
const modified = new Map(
|
||||
modifiedArray.map(file => {
|
||||
const module = this._resolver.getModuleForPath(file);
|
||||
return [file, module];
|
||||
}),
|
||||
const {added, deleted} = await traverseDependencies(
|
||||
modifiedDependencies,
|
||||
this._dependencyGraph,
|
||||
transformerOptions,
|
||||
this._dependencyEdges,
|
||||
this._options.onProgress || undefined,
|
||||
);
|
||||
|
||||
const filesWithChangedDependencies = await Promise.all(
|
||||
modifiedArray.map(this._hasChangedDependencies, this),
|
||||
);
|
||||
const modified = new Map();
|
||||
|
||||
// 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(), reset: false};
|
||||
for (const path of modifiedDependencies) {
|
||||
modified.set(path, this._dependencyGraph.getModuleForPath(path));
|
||||
}
|
||||
|
||||
// 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);
|
||||
for (const path of added) {
|
||||
modified.set(path, this._dependencyGraph.getModuleForPath(path));
|
||||
}
|
||||
|
||||
return {
|
||||
@ -239,146 +272,6 @@ class DeltaCalculator extends EventEmitter {
|
||||
reset: false,
|
||||
};
|
||||
}
|
||||
|
||||
async _hasChangedDependencies(file: string) {
|
||||
const module = this._resolver.getModuleForPath(file);
|
||||
|
||||
if (!this._modules.has(module.path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldDependenciesMap =
|
||||
this._shallowDependencies.get(module.path) || new Set();
|
||||
|
||||
const newDependencies = await this._getShallowDependencies(module);
|
||||
const oldDependencies = new Set(oldDependenciesMap.keys());
|
||||
|
||||
if (!oldDependencies) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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._transformerOptions = response.options.transformer;
|
||||
|
||||
currentDependencies.forEach(module => {
|
||||
const dependencyPairs = response.getResolvedDependencyPairs(module);
|
||||
|
||||
const shallowDependencies = new Map();
|
||||
for (const [relativePath, module] of dependencyPairs) {
|
||||
shallowDependencies.set(relativePath, module.path);
|
||||
}
|
||||
|
||||
this._shallowDependencies.set(module.path, shallowDependencies);
|
||||
|
||||
// Only add it to the delta bundle if it did not exist before.
|
||||
if (!this._modules.has(module.path)) {
|
||||
added.set(module.path, module);
|
||||
this._modules.add(module.path);
|
||||
}
|
||||
});
|
||||
|
||||
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._modules.size) {
|
||||
const currentSet = new Set(currentDependencies.map(dep => dep.path));
|
||||
|
||||
this._modules.forEach(file => {
|
||||
if (currentSet.has(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._modules.delete(file);
|
||||
this._shallowDependencies.delete(file);
|
||||
|
||||
deleted.add(file);
|
||||
});
|
||||
}
|
||||
|
||||
// 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._shallowDependencies.get(module.path) || new Map();
|
||||
|
||||
dependencies.forEach((relativePath, path) => {
|
||||
let inverse = this._inverseDependencies.get(path);
|
||||
|
||||
if (!inverse) {
|
||||
inverse = new Set();
|
||||
this._inverseDependencies.set(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,
|
||||
transformerOptions: this._transformerOptions || 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;
|
||||
|
@ -25,7 +25,7 @@ import type Resolver from '../Resolver';
|
||||
import type {MappingsMap} from '../lib/SourceMap';
|
||||
import type Module from '../node-haste/Module';
|
||||
import type {Options as BundleOptions} from './';
|
||||
import type {ShallowDependencies} from './DeltaCalculator';
|
||||
import type {DependencyEdges} from './traverseDependencies';
|
||||
|
||||
type DeltaEntry = {|
|
||||
+code: string,
|
||||
@ -118,7 +118,7 @@ class DeltaTransformer extends EventEmitter {
|
||||
|
||||
const deltaCalculator = new DeltaCalculator(
|
||||
bundler,
|
||||
resolver,
|
||||
resolver.getDependencyGraph(),
|
||||
bundleOptions,
|
||||
);
|
||||
|
||||
@ -171,14 +171,14 @@ class DeltaTransformer extends EventEmitter {
|
||||
// Calculate the delta of modules.
|
||||
const {modified, deleted, reset} = await this._deltaCalculator.getDelta();
|
||||
|
||||
const transformerOptions = this._deltaCalculator.getTransformerOptions();
|
||||
const dependencyPairs = this._deltaCalculator.getShallowDependencies();
|
||||
const transformerOptions = await this._deltaCalculator.getTransformerOptions();
|
||||
const dependencyEdges = this._deltaCalculator.getDependencyEdges();
|
||||
|
||||
// Get the transformed source code of each modified/added module.
|
||||
const modifiedDelta = await this._transformModules(
|
||||
Array.from(modified.values()),
|
||||
transformerOptions,
|
||||
dependencyPairs,
|
||||
dependencyEdges,
|
||||
);
|
||||
|
||||
deleted.forEach(id => {
|
||||
@ -188,19 +188,17 @@ class DeltaTransformer extends EventEmitter {
|
||||
// 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)
|
||||
? await this._getPrepend(transformerOptions, dependencyEdges)
|
||||
: new Map();
|
||||
|
||||
// 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)
|
||||
? await this._getAppend(dependencyEdges)
|
||||
: new Map();
|
||||
|
||||
// Inverse dependencies are needed for HMR.
|
||||
const inverseDependencies = this._getInverseDependencies(
|
||||
this._deltaCalculator.getInverseDependencies(),
|
||||
);
|
||||
const inverseDependencies = this._getInverseDependencies(dependencyEdges);
|
||||
|
||||
return {
|
||||
pre: prependSources,
|
||||
@ -213,7 +211,7 @@ class DeltaTransformer extends EventEmitter {
|
||||
|
||||
async _getPrepend(
|
||||
transformOptions: JSTransformerOptions,
|
||||
dependencyPairs: ShallowDependencies,
|
||||
dependencyEdges: DependencyEdges,
|
||||
): Promise<DeltaEntries> {
|
||||
// Get all the polyfills from the relevant option params (the
|
||||
// `getPolyfills()` method and the `polyfillModuleNames` variable).
|
||||
@ -240,13 +238,11 @@ class DeltaTransformer extends EventEmitter {
|
||||
return await this._transformModules(
|
||||
modules,
|
||||
transformOptions,
|
||||
dependencyPairs,
|
||||
dependencyEdges,
|
||||
);
|
||||
}
|
||||
|
||||
async _getAppend(
|
||||
dependencyPairs: ShallowDependencies,
|
||||
): Promise<DeltaEntries> {
|
||||
async _getAppend(dependencyEdges: DependencyEdges): Promise<DeltaEntries> {
|
||||
// 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.
|
||||
@ -300,13 +296,13 @@ class DeltaTransformer extends EventEmitter {
|
||||
* Converts the paths in the inverse dependendencies to module ids.
|
||||
*/
|
||||
_getInverseDependencies(
|
||||
inverseDependencies: Map<string, Set<string>>,
|
||||
dependencyEdges: DependencyEdges,
|
||||
): {[key: string]: $ReadOnlyArray<string>} {
|
||||
const output = Object.create(null);
|
||||
|
||||
for (const [key, dependencies] of inverseDependencies) {
|
||||
output[this._getModuleId({path: key})] = Array.from(
|
||||
dependencies,
|
||||
for (const [path, {inverseDependencies}] of dependencyEdges.entries()) {
|
||||
output[this._getModuleId({path})] = Array.from(
|
||||
inverseDependencies,
|
||||
).map(dep => this._getModuleId({path: dep}));
|
||||
}
|
||||
|
||||
@ -316,12 +312,12 @@ class DeltaTransformer extends EventEmitter {
|
||||
async _transformModules(
|
||||
modules: Array<Module>,
|
||||
transformOptions: JSTransformerOptions,
|
||||
dependencyPairs: ShallowDependencies,
|
||||
dependencyEdges: DependencyEdges,
|
||||
): Promise<DeltaEntries> {
|
||||
return new Map(
|
||||
await Promise.all(
|
||||
modules.map(module =>
|
||||
this._transformModule(module, transformOptions, dependencyPairs),
|
||||
this._transformModule(module, transformOptions, dependencyEdges),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -330,18 +326,18 @@ class DeltaTransformer extends EventEmitter {
|
||||
async _transformModule(
|
||||
module: Module,
|
||||
transformOptions: JSTransformerOptions,
|
||||
dependencyPairs: ShallowDependencies,
|
||||
dependencyEdges: DependencyEdges,
|
||||
): Promise<[number, ?DeltaEntry]> {
|
||||
const name = module.getName();
|
||||
const metadata = await this._getMetadata(module, transformOptions);
|
||||
const dependencyPairsForModule =
|
||||
dependencyPairs.get(module.path) || new Map();
|
||||
const edge = dependencyEdges.get(module.path);
|
||||
const dependencyPairs = edge ? edge.dependencies : new Map();
|
||||
|
||||
const wrapped = this._bundleOptions.wrapModules
|
||||
? this._resolver.wrapModule({
|
||||
module,
|
||||
getModuleId: this._getModuleId,
|
||||
dependencyPairs: dependencyPairsForModule,
|
||||
dependencyPairs,
|
||||
dependencyOffsets: metadata.dependencyOffsets || [],
|
||||
name,
|
||||
code: metadata.code,
|
||||
@ -354,7 +350,7 @@ class DeltaTransformer extends EventEmitter {
|
||||
module,
|
||||
this._getModuleId,
|
||||
metadata.code,
|
||||
dependencyPairsForModule,
|
||||
dependencyPairs,
|
||||
metadata.dependencyOffsets || [],
|
||||
),
|
||||
map: metadata.map,
|
||||
|
@ -14,14 +14,19 @@
|
||||
|
||||
jest.mock('../../Bundler');
|
||||
jest.mock('../../Resolver');
|
||||
jest.mock('../traverseDependencies');
|
||||
|
||||
const Bundler = require('../../Bundler');
|
||||
const {EventEmitter} = require('events');
|
||||
const Resolver = require('../../Resolver');
|
||||
|
||||
const DeltaCalculator = require('../DeltaCalculator');
|
||||
const {
|
||||
initialTraverseDependencies,
|
||||
traverseDependencies,
|
||||
} = require('../traverseDependencies');
|
||||
|
||||
describe('DeltaCalculator', () => {
|
||||
const entryModule = createModule({path: '/bundle', name: 'bundle'});
|
||||
const moduleFoo = createModule({path: '/foo', name: 'foo'});
|
||||
const moduleBar = createModule({path: '/bar', name: 'bar'});
|
||||
const moduleBaz = createModule({path: '/baz', name: 'baz'});
|
||||
@ -29,14 +34,13 @@ describe('DeltaCalculator', () => {
|
||||
let deltaCalculator;
|
||||
let fileWatcher;
|
||||
let mockedDependencies;
|
||||
let mockedDependencyTree;
|
||||
|
||||
const bundlerMock = new Bundler();
|
||||
|
||||
const options = {
|
||||
assetPlugins: [],
|
||||
dev: true,
|
||||
entryFile: 'bundle.js',
|
||||
entryFile: 'bundle',
|
||||
entryModuleOnly: false,
|
||||
excludeSource: false,
|
||||
generateSourceMaps: false,
|
||||
@ -68,42 +72,49 @@ describe('DeltaCalculator', () => {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
mockedDependencies = [moduleFoo, moduleBar, moduleBaz];
|
||||
mockedDependencyTree = new Map([[moduleFoo, [moduleBar, moduleBaz]]]);
|
||||
mockedDependencies = [entryModule, moduleFoo, moduleBar, moduleBaz];
|
||||
|
||||
fileWatcher = new EventEmitter();
|
||||
|
||||
Resolver.prototype.getDependencyGraph.mockReturnValue({
|
||||
const dependencyGraph = {
|
||||
getWatcher() {
|
||||
return fileWatcher;
|
||||
},
|
||||
});
|
||||
getAbsolutePath(path) {
|
||||
return '/' + path;
|
||||
},
|
||||
getModuleForPath(path) {
|
||||
return mockedDependencies.filter(dep => dep.path === path)[0];
|
||||
},
|
||||
};
|
||||
|
||||
Resolver.prototype.getModuleForPath.mockImplementation(
|
||||
path => mockedDependencies.filter(dep => dep.path === path)[0],
|
||||
);
|
||||
initialTraverseDependencies.mockImplementationOnce(
|
||||
async (path, dg, opt, edges) => {
|
||||
edges.set('/foo', moduleFoo);
|
||||
|
||||
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.getShallowDependencies.mockImplementation(
|
||||
async module => {
|
||||
const deps = mockedDependencyTree.get(module);
|
||||
return deps ? await Promise.all(deps.map(dep => dep.getName())) : [];
|
||||
return {
|
||||
added: new Set(['/foo', '/bar', '/baz']),
|
||||
deleted: new Set(),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const resolverMock = new Resolver();
|
||||
Bundler.prototype.getTransformOptions.mockImplementation(async () => {
|
||||
return {
|
||||
transformer: {},
|
||||
};
|
||||
});
|
||||
|
||||
deltaCalculator = new DeltaCalculator(bundlerMock, resolverMock, options);
|
||||
deltaCalculator = new DeltaCalculator(
|
||||
bundlerMock,
|
||||
dependencyGraph,
|
||||
options,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
initialTraverseDependencies.mockReset();
|
||||
traverseDependencies.mockReset();
|
||||
});
|
||||
|
||||
it('should start listening for file changes after being initialized', async () => {
|
||||
@ -116,11 +127,12 @@ describe('DeltaCalculator', () => {
|
||||
expect(fileWatcher.listeners('change')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should calculate the initial bundle correctly', async () => {
|
||||
it('should include the entry file when calculating the initial bundle', async () => {
|
||||
const result = await deltaCalculator.getDelta();
|
||||
|
||||
expect(result).toEqual({
|
||||
modified: new Map([
|
||||
['/bundle', entryModule],
|
||||
['/foo', moduleFoo],
|
||||
['/bar', moduleBar],
|
||||
['/baz', moduleBaz],
|
||||
@ -138,20 +150,31 @@ describe('DeltaCalculator', () => {
|
||||
deleted: new Set(),
|
||||
reset: false,
|
||||
});
|
||||
|
||||
expect(traverseDependencies.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should calculate a delta after a simple modification', async () => {
|
||||
// Get initial delta
|
||||
await deltaCalculator.getDelta();
|
||||
|
||||
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
|
||||
|
||||
traverseDependencies.mockReturnValue(
|
||||
Promise.resolve({
|
||||
added: new Set(),
|
||||
deleted: new Set(),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await deltaCalculator.getDelta();
|
||||
|
||||
expect(result).toEqual({
|
||||
modified: new Map([['/foo', moduleFoo]]),
|
||||
deleted: new Set(),
|
||||
reset: false,
|
||||
});
|
||||
|
||||
expect(traverseDependencies.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should calculate a delta after removing a dependency', async () => {
|
||||
@ -160,16 +183,22 @@ describe('DeltaCalculator', () => {
|
||||
|
||||
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
|
||||
|
||||
// Remove moduleBar
|
||||
mockedDependencyTree.set(moduleFoo, [moduleBaz]);
|
||||
mockedDependencies = [moduleFoo, moduleBaz];
|
||||
traverseDependencies.mockReturnValue(
|
||||
Promise.resolve({
|
||||
added: new Set(),
|
||||
deleted: new Set(['/baz']),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await deltaCalculator.getDelta();
|
||||
|
||||
expect(result).toEqual({
|
||||
modified: new Map([['/foo', moduleFoo]]),
|
||||
deleted: new Set(['/bar']),
|
||||
deleted: new Set(['/baz']),
|
||||
reset: false,
|
||||
});
|
||||
|
||||
expect(traverseDependencies.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should calculate a delta after adding/removing dependencies', async () => {
|
||||
@ -178,10 +207,14 @@ describe('DeltaCalculator', () => {
|
||||
|
||||
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
|
||||
|
||||
// Add moduleQux
|
||||
const moduleQux = createModule({path: '/qux', name: 'qux'});
|
||||
mockedDependencyTree.set(moduleFoo, [moduleQux]);
|
||||
mockedDependencies = [moduleFoo, moduleQux];
|
||||
|
||||
traverseDependencies.mockReturnValue(
|
||||
Promise.resolve({
|
||||
added: new Set([moduleQux.path]),
|
||||
deleted: new Set(['/bar', '/baz']),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await deltaCalculator.getDelta();
|
||||
expect(result).toEqual({
|
||||
@ -191,27 +224,6 @@ describe('DeltaCalculator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate the shallowDependencies correctly after adding/removing dependencies', async () => {
|
||||
// Get initial delta
|
||||
await deltaCalculator.getDelta();
|
||||
|
||||
expect(deltaCalculator.getShallowDependencies().size).toBe(3);
|
||||
|
||||
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
|
||||
|
||||
// Add moduleQux
|
||||
const moduleQux = createModule({path: '/qux', name: 'qux'});
|
||||
mockedDependencyTree.set(moduleFoo, [moduleBar, moduleBaz, moduleQux]);
|
||||
mockedDependencies = [moduleFoo, moduleBar, moduleBaz, moduleQux];
|
||||
|
||||
await deltaCalculator.getDelta();
|
||||
|
||||
const shallowDependencies = deltaCalculator.getShallowDependencies();
|
||||
|
||||
expect(shallowDependencies.size).toBe(4);
|
||||
expect(shallowDependencies.get('/foo').get('qux')).toEqual('/qux');
|
||||
});
|
||||
|
||||
it('should emit an event when there is a relevant file change', async done => {
|
||||
await deltaCalculator.getDelta();
|
||||
|
||||
@ -220,20 +232,6 @@ describe('DeltaCalculator', () => {
|
||||
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
|
||||
});
|
||||
|
||||
it('should not emit an event when there is a file changed outside the bundle', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const onChangeFile = jest.fn();
|
||||
await deltaCalculator.getDelta();
|
||||
|
||||
deltaCalculator.on('change', onChangeFile);
|
||||
fileWatcher.emit('change', {eventsQueue: [{filePath: '/another'}]});
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(onChangeFile.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not emit an event when there is a file deleted', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
@ -254,9 +252,7 @@ describe('DeltaCalculator', () => {
|
||||
|
||||
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
|
||||
|
||||
Bundler.prototype.getShallowDependencies.mockImplementation(async () => {
|
||||
throw new Error('error');
|
||||
});
|
||||
traverseDependencies.mockReturnValue(Promise.reject(new Error()));
|
||||
|
||||
await expect(deltaCalculator.getDelta()).rejects.toBeInstanceOf(Error);
|
||||
|
||||
|
@ -14,17 +14,6 @@
|
||||
|
||||
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
|
||||
import type DependencyGraph from '../node-haste/DependencyGraph';
|
||||
import type {
|
||||
InverseDependencies,
|
||||
ModulePaths,
|
||||
ShallowDependencies,
|
||||
} from './DeltaCalculator';
|
||||
|
||||
export type Caches = {|
|
||||
inverseDependencies: InverseDependencies,
|
||||
modules: ModulePaths,
|
||||
shallowDependencies: ShallowDependencies,
|
||||
|};
|
||||
|
||||
export type DependencyEdge = {|
|
||||
dependencies: Map<string, string>,
|
||||
|
Loading…
x
Reference in New Issue
Block a user