Decouple the dependency traversal logic from Bundler/Dependencygraph/Module

Summary:
This commit refactors the metro bundling logic to make the traversal of dependencies much more generic, to not depend on any assumption regarding dependency resolution, haste, or even about the transformer/caching system.

So, the actual API of the `DeltaBundler` module (which will be extracted as a separate package in the near future) ends up being like this:

```
const graph = await deltaBundler.buildGraph(
  ['/root/entryPoint1.js', '/root/entryPoint2.js', /* ... */],
  {
    dependencyResolver: (from, to) => require('resolve').sync(to, {from}), // Use the standard nodejs resolver
    transformFn: filePath => await transformFileUsingWhateverIPrefer(filePath),
  },
);
```

The only part that is still coupled with the `DependencyGraph` is the file listener (which is tied to JestHasteMap via `DependencyGraph`), which is going to be decoupled soon.

From here, and once we have the new caching system fully rolled out and the old cache logic removed, we'll incorporate `jest-worker` and the caching system directly into the build graph logic, to end up extracting the whole `DeltaBundler` logic into its own package.

From there we can potentially reuse it to create simpler/more focused bundlers for web/etc.

Reviewed By: davidaurelio

Differential Revision: D7617143

fbshipit-source-id: b7e1a71cd95043f6232b07f2e8c45dcd49f089f2
This commit is contained in:
Rafael Oleza 2018-04-18 12:09:02 -07:00 committed by Facebook Github Bot
parent 7cdbdffa96
commit 2446fe2211
17 changed files with 567 additions and 877 deletions

View File

@ -41,14 +41,13 @@ class DeltaBundler {
this._deltaCalculators = new Map();
}
async buildGraph(options: Options): Promise<Graph> {
async buildGraph(
entryPoints: $ReadOnlyArray<string>,
options: Options,
): Promise<Graph> {
const depGraph = await this._bundler.getDependencyGraph();
const deltaCalculator = new DeltaCalculator(
this._bundler,
depGraph,
options,
);
const deltaCalculator = new DeltaCalculator(entryPoints, depGraph, options);
await deltaCalculator.getDelta({reset: true});
const graph = deltaCalculator.getGraph();

View File

@ -17,13 +17,8 @@ const {
} = require('./traverseDependencies');
const {EventEmitter} = require('events');
import type Bundler from '../Bundler';
import type {
Options as JSTransformerOptions,
CustomTransformOptions,
} from '../JSTransformer/worker';
import type DependencyGraph from '../node-haste/DependencyGraph';
import type {DependencyEdge, Graph} from './traverseDependencies';
import type {DependencyEdge, Graph, Options} from './traverseDependencies';
export type DeltaResult = {|
+modified: Map<string, DependencyEdge>,
@ -31,19 +26,7 @@ export type DeltaResult = {|
+reset: boolean,
|};
export type {Graph} from './traverseDependencies';
export type Options = {|
+assetPlugins: Array<string>,
+customTransformOptions: CustomTransformOptions,
+dev: boolean,
+entryPoints: $ReadOnlyArray<string>,
+hot: boolean,
+minify: boolean,
+onProgress: ?(doneCont: number, totalCount: number) => mixed,
+platform: ?string,
+type: 'module' | 'script',
|};
export type {Graph, Options} from './traverseDependencies';
/**
* This class is in charge of calculating the delta of changed modules that
@ -52,10 +35,8 @@ export type Options = {|
* traverse the whole dependency tree for trivial small changes.
*/
class DeltaCalculator extends EventEmitter {
_bundler: Bundler;
_dependencyGraph: DependencyGraph;
_options: Options;
_transformerOptions: ?JSTransformerOptions;
_currentBuildPromise: ?Promise<DeltaResult>;
_deletedFiles: Set<string> = new Set();
@ -64,19 +45,18 @@ class DeltaCalculator extends EventEmitter {
_graph: Graph;
constructor(
bundler: Bundler,
entryPoints: $ReadOnlyArray<string>,
dependencyGraph: DependencyGraph,
options: Options,
) {
super();
this._bundler = bundler;
this._options = options;
this._dependencyGraph = dependencyGraph;
this._graph = {
dependencies: new Map(),
entryPoints: this._options.entryPoints,
entryPoints,
};
this._dependencyGraph
@ -97,7 +77,7 @@ class DeltaCalculator extends EventEmitter {
// Clean up all the cache data structures to deallocate memory.
this._graph = {
dependencies: new Map(),
entryPoints: this._options.entryPoints,
entryPoints: this._graph.entryPoints,
};
this._modifiedFiles = new Set();
this._deletedFiles = new Set();
@ -170,72 +150,6 @@ class DeltaCalculator extends EventEmitter {
return result;
}
/**
* Returns the options object that is used by the transformer to parse
* 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).
*/
async getTransformerOptions(): Promise<JSTransformerOptions> {
if (!this._transformerOptions) {
this._transformerOptions = await this._calcTransformerOptions();
}
return this._transformerOptions;
}
async _calcTransformerOptions(): Promise<JSTransformerOptions> {
const {
enableBabelRCLookup,
projectRoot,
} = this._bundler.getGlobalTransformOptions();
const transformOptionsForBlacklist = {
assetDataPlugins: this._options.assetPlugins,
customTransformOptions: this._options.customTransformOptions,
enableBabelRCLookup,
dev: this._options.dev,
hot: this._options.hot,
inlineRequires: false,
minify: this._options.minify,
platform: this._options.platform,
projectRoot,
};
// When we're processing scripts, we don't need to calculate any
// inlineRequires information, since scripts by definition don't have
// requires().
if (this._options.type === 'script') {
return {
...transformOptionsForBlacklist,
inlineRequires: false,
};
}
const {
inlineRequires,
} = await this._bundler.getTransformOptionsForEntryFiles(
this._options.entryPoints,
{dev: this._options.dev, platform: this._options.platform},
async path => {
const {added} = await initialTraverseDependencies(
{
dependencies: new Map(),
entryPoints: [path],
},
this._dependencyGraph,
{...transformOptionsForBlacklist, type: this._options.type},
);
return Array.from(added.keys());
},
);
// $FlowIssue #23854098 - Object.assign() loses the strictness of an object in flow
return {
...transformOptionsForBlacklist,
inlineRequires: inlineRequires || false,
};
}
/**
* Returns the graph with all the dependency edges. Each edge contains the
* needed information to do the traversing (dependencies, inverseDependencies)
@ -278,17 +192,10 @@ class DeltaCalculator extends EventEmitter {
modifiedFiles: Set<string>,
deletedFiles: Set<string>,
): Promise<DeltaResult> {
const transformerOptions = {
...(await this.getTransformerOptions()),
type: this._options.type,
};
if (!this._graph.dependencies.size) {
const {added} = await initialTraverseDependencies(
this._graph,
this._dependencyGraph,
transformerOptions,
this._options.onProgress || undefined,
this._options,
);
return {
@ -320,10 +227,8 @@ class DeltaCalculator extends EventEmitter {
const {added, deleted} = await traverseDependencies(
modifiedDependencies,
this._dependencyGraph,
transformerOptions,
this._graph,
this._options.onProgress || undefined,
this._options,
);
return {

View File

@ -18,17 +18,15 @@ const {
traverseDependencies,
} = require('../traverseDependencies');
const Bundler = require('../../Bundler');
const {EventEmitter} = require('events');
const DeltaCalculator = require('../DeltaCalculator');
const getTransformOptions = require('../../__fixtures__/getTransformOptions');
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'});
const entryModule = {path: '/bundle', name: 'bundle'};
const moduleFoo = {path: '/foo', name: 'foo'};
const moduleBar = {path: '/bar', name: 'bar'};
const moduleBaz = {path: '/baz', name: 'baz'};
let edgeModule;
let edgeFoo;
@ -37,8 +35,6 @@ describe('DeltaCalculator', () => {
let deltaCalculator;
let fileWatcher;
let mockedDependencies;
let bundlerMock;
const options = {
assetPlugins: [],
@ -55,94 +51,58 @@ describe('DeltaCalculator', () => {
sourceMapUrl: undefined,
};
function createModule({path, name, isAsset, isJSON}) {
return {
path,
name,
getName() {
return name;
},
isAsset() {
return !!isAsset;
},
};
}
beforeEach(async () => {
bundlerMock = new Bundler();
mockedDependencies = [entryModule, moduleFoo, moduleBar, moduleBaz];
fileWatcher = new EventEmitter();
const dependencyGraph = {
getWatcher() {
return fileWatcher;
},
getAbsolutePath(path) {
return '/' + path;
},
getModuleForPath(path) {
return mockedDependencies.filter(dep => dep.path === path)[0];
},
};
initialTraverseDependencies.mockImplementationOnce(
async (graph, dg, opt) => {
edgeModule = {
...entryModule,
dependencies: new Map([
['foo', '/foo'],
['bar', '/bar'],
['baz', '/baz'],
]),
};
edgeFoo = {
...moduleFoo,
dependencies: new Map(),
inverseDependencies: ['/bundle'],
};
edgeBar = {
...moduleBar,
dependencies: new Map(),
inverseDependencies: ['/bundle'],
};
edgeBaz = {
...moduleBaz,
dependencies: new Map(),
inverseDependencies: ['/bundle'],
};
initialTraverseDependencies.mockImplementationOnce(async (graph, opt) => {
edgeModule = {
output: entryModule,
dependencies: new Map([
['foo', '/foo'],
['bar', '/bar'],
['baz', '/baz'],
]),
};
edgeFoo = {
output: moduleFoo,
dependencies: new Map(),
inverseDependencies: ['/bundle'],
};
edgeBar = {
output: moduleBar,
dependencies: new Map(),
inverseDependencies: ['/bundle'],
};
edgeBaz = {
output: moduleBaz,
dependencies: new Map(),
inverseDependencies: ['/bundle'],
};
graph.dependencies.set('/bundle', edgeModule);
graph.dependencies.set('/foo', edgeFoo);
graph.dependencies.set('/bar', edgeBar);
graph.dependencies.set('/baz', edgeBaz);
graph.dependencies.set('/bundle', edgeModule);
graph.dependencies.set('/foo', edgeFoo);
graph.dependencies.set('/bar', edgeBar);
graph.dependencies.set('/baz', edgeBaz);
return {
added: new Map([
['/bundle', edgeModule],
['/foo', edgeFoo],
['/bar', edgeBar],
['/baz', edgeBaz],
]),
deleted: new Set(),
};
},
);
Bundler.prototype.getGlobalTransformOptions.mockReturnValue({
enableBabelRCLookup: false,
projectRoot: '/foo',
return {
added: new Map([
['/bundle', edgeModule],
['/foo', edgeFoo],
['/bar', edgeBar],
['/baz', edgeBaz],
]),
deleted: new Set(),
};
});
Bundler.prototype.getTransformOptionsForEntryFiles.mockReturnValue(
Promise.resolve({
inlineRequires: false,
}),
);
deltaCalculator = new DeltaCalculator(
bundlerMock,
[entryModule.path],
dependencyGraph,
options,
);
@ -264,12 +224,12 @@ describe('DeltaCalculator', () => {
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
const moduleQux = createModule({path: '/qux', name: 'qux'});
const edgeQux = {...moduleQux, inverseDependencies: []};
const edgeQux = {
output: {path: '/qux', name: 'qux'},
inverseDependencies: [],
};
mockedDependencies.push(moduleQux);
traverseDependencies.mockImplementation(async (path, dg, opt, graph) => {
traverseDependencies.mockImplementation(async (path, graph, options) => {
graph.dependencies.set('/qux', edgeQux);
return {
@ -390,65 +350,4 @@ describe('DeltaCalculator', () => {
expect(graph.dependencies.size).toEqual(numDependencies);
});
describe('getTransformerOptions()', () => {
it('should calculate the transform options correctly', async () => {
expect(await deltaCalculator.getTransformerOptions()).toEqual({
assetDataPlugins: [],
dev: true,
enableBabelRCLookup: false,
hot: true,
inlineRequires: false,
minify: false,
platform: 'ios',
projectRoot: '/foo',
});
});
it('should return the same params as the standard options', async () => {
const options = await deltaCalculator.getTransformerOptions();
expect(Object.keys(options).sort()).toEqual(
Object.keys(await getTransformOptions()).sort(),
);
});
it('should handle inlineRequires=true correctly', async () => {
Bundler.prototype.getTransformOptionsForEntryFiles.mockReturnValue(
Promise.resolve({
inlineRequires: true,
}),
);
expect(await deltaCalculator.getTransformerOptions()).toEqual({
assetDataPlugins: [],
dev: true,
enableBabelRCLookup: false,
hot: true,
inlineRequires: true,
minify: false,
platform: 'ios',
projectRoot: '/foo',
});
});
it('should handle an inline requires blacklist correctly', async () => {
Bundler.prototype.getTransformOptionsForEntryFiles.mockReturnValue(
Promise.resolve({
inlineRequires: {blacklist: {'/bar': true, '/baz': true}},
}),
);
expect(await deltaCalculator.getTransformerOptions()).toEqual({
assetDataPlugins: [],
dev: true,
enableBabelRCLookup: false,
hot: true,
inlineRequires: {blacklist: {'/bar': true, '/baz': true}},
minify: false,
platform: 'ios',
projectRoot: '/foo',
});
});
});
});

View File

@ -1,74 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`traverseDependencies Progress updates increases the number of discover/finished modules in steps of one 1`] = `
Array [
Array [
0,
1,
],
Array [
0,
2,
],
Array [
0,
3,
],
Array [
0,
4,
],
Array [
0,
5,
],
Array [
0,
6,
],
Array [
0,
7,
],
Array [
1,
7,
],
Array [
2,
7,
],
Array [
2,
8,
],
Array [
3,
8,
],
Array [
4,
8,
],
Array [
5,
8,
],
Array [
6,
8,
],
Array [
7,
8,
],
Array [
8,
8,
],
]
`;
exports[`traverseDependencies file watch updating should recover from multiple modules with the same name 1`] = `
Array [
Object {

View File

@ -44,10 +44,11 @@ beforeEach(() => {
});
describe('traverseDependencies', function() {
let fs;
let Module;
let traverseDependencies;
let transformHelpers;
let defaults;
let emptyTransformOptions;
let UnableToResolveError;
async function getOrderedDependenciesAsJSON(
@ -63,10 +64,29 @@ describe('traverseDependencies', function() {
entryPoints: [entryPath],
};
const bundler = {
getDependencyGraph() {
return Promise.resolve(dgraph);
},
};
const {added} = await traverseDependencies.initialTraverseDependencies(
graph,
dgraph,
{...emptyTransformOptions, platform},
{
resolve: await transformHelpers.getResolveDependencyFn(
bundler,
platform,
),
transform: async path => {
let dependencies = [];
const sourceCode = fs.readFileSync(path, 'utf8');
if (!path.endsWith('.json')) {
dependencies = extractDependencies(sourceCode);
}
return {dependencies, output: {code: sourceCode}};
},
},
);
const dependencies = recursive
@ -94,13 +114,14 @@ describe('traverseDependencies', function() {
jest.resetModules();
jest.mock('fs', () => new (require('metro-memory-fs'))());
fs = require('fs');
Module = require('../../node-haste/Module');
traverseDependencies = require('../traverseDependencies');
transformHelpers = require('../../lib/transformHelpers');
({
UnableToResolveError,
} = require('../../node-haste/DependencyGraph/ModuleResolution'));
emptyTransformOptions = {transformer: {transform: {}}};
defaults = {
assetExts: ['png', 'jpg'],
// This pattern is not expected to match anything.
@ -2336,6 +2357,9 @@ describe('traverseDependencies', function() {
jest.resetModules();
jest.mock('path', () => require.requireActual('path').win32);
jest.mock('fs', () => new (require('metro-memory-fs'))('win32'));
fs = require('fs');
require('os').tmpdir = () => 'c:\\tmp';
DependencyGraph = require('../../node-haste/DependencyGraph');
processDgraph = processDgraphFor.bind(null, DependencyGraph);
@ -3425,6 +3449,8 @@ describe('traverseDependencies', function() {
jest.mock('path', () => require.requireActual('path').win32);
jest.mock('fs', () => new (require('metro-memory-fs'))('win32'));
require('os').tmpdir = () => 'c:\\tmp';
fs = require('fs');
DependencyGraph = require('../../node-haste/DependencyGraph');
processDgraph = processDgraphFor.bind(null, DependencyGraph);
({
@ -5039,76 +5065,6 @@ describe('traverseDependencies', function() {
});
});
describe('Progress updates', () => {
let dependencyGraph, onProgress;
function makeModule(id, dependencies = []) {
return (
`
/**
* @providesModule ${id}
*/\n` +
dependencies.map(d => `require(${JSON.stringify(d)});`).join('\n')
);
}
function getDependencies() {
return traverseDependencies.initialTraverseDependencies(
{
dependencies: new Map(),
entryPoints: ['/root/index.js'],
},
dependencyGraph,
emptyTransformOptions,
onProgress,
);
}
beforeEach(function() {
onProgress = jest.genMockFn();
setMockFileSystem({
root: {
'index.js': makeModule('index', ['a', 'b']),
'a.js': makeModule('a', ['c', 'd']),
'b.js': makeModule('b', ['d', 'e']),
'c.js': makeModule('c'),
'd.js': makeModule('d', ['f']),
'e.js': makeModule('e', ['f']),
'f.js': makeModule('f', ['g']),
'g.js': makeModule('g'),
},
});
const DependencyGraph = require('../../node-haste/DependencyGraph');
return DependencyGraph.load(
{
...defaults,
projectRoots: ['/root'],
},
false /* since we're mocking the filesystem, we cannot use watchman */,
).then(dg => {
dependencyGraph = dg;
});
});
afterEach(() => {
dependencyGraph.end();
});
it('calls back for each finished module', async () => {
await getDependencies();
// We get a progress change twice per dependency
// (when we discover it and when we process it).
expect(onProgress.mock.calls.length).toBe(8 * 2);
});
it('increases the number of discover/finished modules in steps of one', async () => {
await getDependencies();
expect(onProgress.mock.calls).toMatchSnapshot();
});
});
describe('Asset module dependencies', () => {
let DependencyGraph;
let processDgraph;

View File

@ -21,6 +21,7 @@ let mockedDependencies;
let mockedDependencyTree;
let files = new Set();
let graph;
let options;
let entryModule;
let moduleFoo;
@ -29,63 +30,47 @@ let moduleBaz;
const Actions = {
modifyFile(path) {
if (mockedDependencyTree.get(path)) {
if (mockedDependencies.has(path)) {
files.add(path);
}
},
moveFile(from, to) {
const module = Actions.createFile(to);
Actions.createFile(to);
Actions.deleteFile(from);
return module;
},
deleteFile(path) {
const dependency = dependencyGraph.getModuleForPath(path);
if (dependency) {
mockedDependencies.delete(dependency);
}
mockedDependencies.delete(path);
},
createFile(path) {
const module = createModule({
path,
name: path.replace('/', ''),
});
mockedDependencies.add(module);
mockedDependencies.add(path);
mockedDependencyTree.set(path, []);
return module;
return path;
},
addDependency(path, dependencyPath, position, name = null) {
let dependency = dependencyGraph.getModuleForPath(dependencyPath);
if (!dependency) {
dependency = Actions.createFile(dependencyPath);
}
const deps = mockedDependencyTree.get(path);
name = name || dependency.name;
name = name || dependencyPath.replace('/', '');
if (position == null) {
deps.push({name, dependency});
deps.push({name, path: dependencyPath});
} else {
deps.splice(position, 0, {name, dependency});
deps.splice(position, 0, {name, path: dependencyPath});
}
mockedDependencyTree.set(path, deps);
mockedDependencies.add(dependency);
mockedDependencies.add(dependencyPath);
files.add(path);
},
removeDependency(path, dependencyPath) {
const dep = dependencyGraph.getModuleForPath(dependencyPath);
const deps = mockedDependencyTree.get(path);
const index = deps.findIndex(({dependency}) => dep === dependency);
const index = deps.findIndex(({path}) => path === dependencyPath);
if (index !== -1) {
deps.splice(index, 1);
mockedDependencyTree.set(path, deps);
@ -102,30 +87,6 @@ function deferred(value) {
return {promise, resolve: () => resolve(value)};
}
function createModule({path, name}) {
return {
path,
name,
isAsset() {
return false;
},
isPolyfill() {
return false;
},
async read() {
const deps = mockedDependencyTree.get(path);
const dependencies = deps ? deps.map(dep => dep.name) : [];
return {
code: '// code',
map: [],
source: '// source',
dependencies,
};
},
};
}
function getPaths({added, deleted}) {
const addedPaths = [...added.values()].map(edge => edge.path);
@ -159,6 +120,33 @@ beforeEach(async () => {
},
};
options = {
resolve: (from, to) => {
const deps = mockedDependencyTree.get(from);
const {path} = deps.filter(dep => dep.name === to)[0];
if (!mockedDependencies.has(path)) {
throw new Error(`Dependency not found: ${path}->${to}`);
}
return path;
},
transform: path => {
const deps = mockedDependencyTree.get(path);
const dependencies = deps ? deps.map(dep => dep.name) : [];
return {
dependencies,
output: {
code: '// code',
map: [],
source: '// source',
type: 'module',
},
};
},
onProgress: null,
};
// Generate the initial dependency graph.
entryModule = Actions.createFile('/bundle');
moduleFoo = Actions.createFile('/foo');
@ -178,7 +166,7 @@ beforeEach(async () => {
});
it('should do the initial traversal correctly', async () => {
const result = await initialTraverseDependencies(graph, dependencyGraph, {});
const result = await initialTraverseDependencies(graph, options);
expect(getPaths(result)).toEqual({
added: new Set(['/bundle', '/foo', '/bar', '/baz']),
@ -189,12 +177,10 @@ it('should do the initial traversal correctly', async () => {
});
it('should return an empty result when there are no changes', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
expect(
getPaths(
await traverseDependencies(['/bundle'], dependencyGraph, {}, graph),
),
getPaths(await traverseDependencies(['/bundle'], graph, options)),
).toEqual({
added: new Set(['/bundle']),
deleted: new Set(),
@ -202,14 +188,12 @@ it('should return an empty result when there are no changes', async () => {
});
it('should return a removed dependency', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
Actions.removeDependency('/foo', '/bar');
expect(
getPaths(
await traverseDependencies([...files], dependencyGraph, {}, graph),
),
getPaths(await traverseDependencies([...files], graph, options)),
).toEqual({
added: new Set(['/foo']),
deleted: new Set(['/bar']),
@ -217,16 +201,14 @@ it('should return a removed dependency', async () => {
});
it('should return added/removed dependencies', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
Actions.addDependency('/foo', '/qux');
Actions.removeDependency('/foo', '/bar');
Actions.removeDependency('/foo', '/baz');
expect(
getPaths(
await traverseDependencies([...files], dependencyGraph, {}, graph),
),
getPaths(await traverseDependencies([...files], graph, options)),
).toEqual({
added: new Set(['/foo', '/qux']),
deleted: new Set(['/bar', '/baz']),
@ -234,7 +216,7 @@ it('should return added/removed dependencies', async () => {
});
it('should return added modules before the modified ones', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
Actions.addDependency('/foo', '/qux');
Actions.modifyFile('/bar');
@ -243,40 +225,67 @@ it('should return added modules before the modified ones', async () => {
// extect.toEqual() does not check order of Sets/Maps, so we need to convert
// it to an array.
expect([
...getPaths(
await traverseDependencies([...files], dependencyGraph, {}, graph),
).added,
...getPaths(await traverseDependencies([...files], graph, options)).added,
]).toEqual(['/qux', '/foo', '/bar', '/baz']);
});
it('should retry to traverse the dependencies as it was after getting an error', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
Actions.deleteFile(moduleBar.path);
Actions.deleteFile(moduleBar);
await expect(
traverseDependencies(['/foo'], dependencyGraph, {}, graph),
traverseDependencies(['/foo'], graph, options),
).rejects.toBeInstanceOf(Error);
// Second time that the traversal of dependencies we still have to throw an
// error (no matter if no file has been changed).
await expect(
traverseDependencies(['/foo'], dependencyGraph, {}, graph),
traverseDependencies(['/foo'], graph, options),
).rejects.toBeInstanceOf(Error);
});
describe('Progress updates', () => {
it('calls back for each finished module', async () => {
const onProgress = jest.fn();
await initialTraverseDependencies(graph, {...options, onProgress});
// We get a progress change twice per dependency
// (when we discover it and when we process it).
expect(onProgress.mock.calls.length).toBe(mockedDependencies.size * 2);
});
it('increases the number of discover/finished modules in steps of one', async () => {
const onProgress = jest.fn();
await initialTraverseDependencies(graph, {...options, onProgress});
const lastCall = {
num: 0,
total: 0,
};
for (const call of onProgress.mock.calls) {
expect(call[0]).toBeGreaterThanOrEqual(lastCall.num);
expect(call[1]).toBeGreaterThanOrEqual(lastCall.total);
expect(call[0] + call[1]).toEqual(lastCall.num + lastCall.total + 1);
lastCall.num = call[0];
lastCall.total = call[1];
}
});
});
describe('edge cases', () => {
it('should handle renames correctly', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
Actions.removeDependency('/foo', '/baz');
Actions.moveFile('/baz', '/qux');
Actions.addDependency('/foo', '/qux');
expect(
getPaths(
await traverseDependencies([...files], dependencyGraph, {}, graph),
),
getPaths(await traverseDependencies([...files], graph, options)),
).toEqual({
added: new Set(['/foo', '/qux']),
deleted: new Set(['/baz']),
@ -284,7 +293,7 @@ describe('edge cases', () => {
});
it('should not try to remove wrong dependencies when renaming files', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
// Rename /foo to /foo-renamed, but keeping all its dependencies.
Actions.addDependency('/bundle', '/foo-renamed');
@ -295,9 +304,7 @@ describe('edge cases', () => {
Actions.addDependency('/foo-renamed', '/baz');
expect(
getPaths(
await traverseDependencies([...files], dependencyGraph, {}, graph),
),
getPaths(await traverseDependencies([...files], graph, options)),
).toEqual({
added: new Set(['/bundle', '/foo-renamed']),
deleted: new Set(['/foo']),
@ -305,15 +312,13 @@ describe('edge cases', () => {
});
it('modify a file and delete it afterwards', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
Actions.modifyFile('/baz');
Actions.removeDependency('/foo', '/baz');
expect(
getPaths(
await traverseDependencies([...files], dependencyGraph, {}, graph),
),
getPaths(await traverseDependencies([...files], graph, options)),
).toEqual({
added: new Set(['/foo']),
deleted: new Set(['/baz']),
@ -321,15 +326,13 @@ describe('edge cases', () => {
});
it('move a file to a different folder', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
Actions.addDependency('/foo', '/baz-moved');
Actions.removeDependency('/foo', '/baz');
expect(
getPaths(
await traverseDependencies([...files], dependencyGraph, {}, graph),
),
getPaths(await traverseDependencies([...files], graph, options)),
).toEqual({
added: new Set(['/foo', '/baz-moved']),
deleted: new Set(['/baz']),
@ -337,20 +340,18 @@ describe('edge cases', () => {
});
it('maintain the order of module dependencies consistent', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
Actions.addDependency('/foo', '/qux', 0);
expect(
getPaths(
await traverseDependencies([...files], dependencyGraph, {}, graph),
),
getPaths(await traverseDependencies([...files], graph, options)),
).toEqual({
added: new Set(['/foo', '/qux']),
deleted: new Set(),
});
expect([...graph.dependencies.get(moduleFoo.path).dependencies]).toEqual([
expect([...graph.dependencies.get(moduleFoo).dependencies]).toEqual([
['qux', '/qux'],
['bar', '/bar'],
['baz', '/baz'],
@ -358,21 +359,19 @@ describe('edge cases', () => {
});
it('should create two entries when requiring the same file in different forms', async () => {
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
// We're adding a new reference from bundle to foo.
Actions.addDependency('/bundle', '/foo', 0, 'foo.js');
expect(
getPaths(
await traverseDependencies([...files], dependencyGraph, {}, graph),
),
getPaths(await traverseDependencies([...files], graph, options)),
).toEqual({
added: new Set(['/bundle']),
deleted: new Set(),
});
expect([...graph.dependencies.get(entryModule.path).dependencies]).toEqual([
expect([...graph.dependencies.get(entryModule).dependencies]).toEqual([
['foo.js', '/foo'],
['foo', '/foo'],
]);
@ -392,7 +391,7 @@ describe('edge cases', () => {
entryPoints: ['/bundle', '/bundle-2'],
};
await initialTraverseDependencies(graph, dependencyGraph, {});
await initialTraverseDependencies(graph, options);
expect([...graph.dependencies.keys()]).toEqual([
'/bundle',
@ -449,9 +448,7 @@ describe('edge cases', () => {
expect(
Array.from(
getPaths(
await initialTraverseDependencies(graph, dependencyGraph, {}),
).added,
getPaths(await initialTraverseDependencies(graph, options)).added,
),
).toEqual(['/bundle', '/foo', '/baz', '/bar']);
}
@ -459,14 +456,11 @@ describe('edge cases', () => {
// Create a dependency tree where moduleBaz has two inverse dependencies.
mockedDependencyTree = new Map([
[
entryModule.path,
[
{name: 'foo', dependency: moduleFoo},
{name: 'bar', dependency: moduleBar},
],
entryModule,
[{name: 'foo', path: moduleFoo}, {name: 'bar', path: moduleBar}],
],
[moduleFoo.path, [{name: 'baz', dependency: moduleBaz}]],
[moduleBar.path, [{name: 'baz', dependency: moduleBaz}]],
[moduleFoo, [{name: 'baz', path: moduleBaz}]],
[moduleBar, [{name: 'baz', path: moduleBaz}]],
]);
// Test that even when having different modules taking longer, the order
@ -477,39 +471,6 @@ describe('edge cases', () => {
mockShallowDependencies('/bar', '/foo');
await assertOrder();
});
it('should simplify inlineRequires transform option', async () => {
jest.spyOn(entryModule, 'read');
jest.spyOn(moduleFoo, 'read');
jest.spyOn(moduleBar, 'read');
jest.spyOn(moduleBaz, 'read');
const transformOptions = {
inlineRequires: {
blacklist: {
'/baz': true,
},
},
};
await initialTraverseDependencies(graph, dependencyGraph, transformOptions);
expect(entryModule.read).toHaveBeenCalledWith({inlineRequires: true});
expect(moduleFoo.read).toHaveBeenCalledWith({inlineRequires: true});
expect(moduleBar.read).toHaveBeenCalledWith({inlineRequires: true});
expect(moduleBaz.read).toHaveBeenCalledWith({inlineRequires: false});
moduleFoo.read.mockClear();
await traverseDependencies(
['/foo'],
dependencyGraph,
transformOptions,
graph,
);
expect(moduleFoo.read).toHaveBeenCalledWith({inlineRequires: true});
});
});
describe('reorderGraph', () => {

View File

@ -10,11 +10,6 @@
'use strict';
const removeInlineRequiresBlacklistFromOptions = require('../lib/removeInlineRequiresBlacklistFromOptions');
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
import type DependencyGraph from '../node-haste/DependencyGraph';
import type Module from '../node-haste/Module';
import type {MetroSourceMapSegmentTuple} from 'metro-source-map';
export type DependencyType = 'module' | 'script' | 'asset';
@ -24,10 +19,10 @@ export type DependencyEdge = {|
inverseDependencies: Set<string>,
path: string,
output: {
code: string,
map: Array<MetroSourceMapSegmentTuple>,
source: string,
type: DependencyType,
+code: string,
+map: Array<MetroSourceMapSegmentTuple>,
+source: string,
+type: DependencyType,
},
|};
@ -52,9 +47,20 @@ type Delta = {
deleted: Set<string>,
};
export type TransformOptions = {|
...JSTransformerOptions,
type: 'module' | 'script',
export type TransformFn = string => Promise<{
dependencies: $ReadOnlyArray<string>,
output: {
+code: string,
+map: Array<MetroSourceMapSegmentTuple>,
+source: string,
+type: DependencyType,
},
}>;
export type Options = {|
resolve: (from: string, to: string) => string,
transform: TransformFn,
onProgress: ?(numProcessed: number, total: number) => mixed,
|};
/**
@ -73,10 +79,8 @@ export type TransformOptions = {|
*/
async function traverseDependencies(
paths: $ReadOnlyArray<string>,
dependencyGraph: DependencyGraph,
transformOptions: TransformOptions,
graph: Graph,
onProgress?: (numProcessed: number, total: number) => mixed = () => {},
options: Options,
): Promise<Result> {
const delta = {
added: new Map(),
@ -94,14 +98,7 @@ async function traverseDependencies(
delta.modified.set(edge.path, edge);
await traverseDependenciesForSingleFile(
edge,
dependencyGraph,
transformOptions,
graph,
delta,
onProgress,
);
await traverseDependenciesForSingleFile(edge, graph, delta, options);
}),
);
@ -140,27 +137,11 @@ async function traverseDependencies(
async function initialTraverseDependencies(
graph: Graph,
dependencyGraph: DependencyGraph,
transformOptions: TransformOptions,
onProgress?: (numProcessed: number, total: number) => mixed = () => {},
options: Options,
): Promise<Result> {
graph.entryPoints.forEach(entryPoint =>
createEdge(
dependencyGraph.getModuleForPath(
entryPoint,
transformOptions.type === 'script',
),
graph,
),
);
graph.entryPoints.forEach(entryPoint => createEdge(entryPoint, graph));
await traverseDependencies(
graph.entryPoints,
dependencyGraph,
transformOptions,
graph,
onProgress,
);
await traverseDependencies(graph.entryPoints, graph, options);
reorderGraph(graph);
@ -172,75 +153,55 @@ async function initialTraverseDependencies(
async function traverseDependenciesForSingleFile(
edge: DependencyEdge,
dependencyGraph: DependencyGraph,
transformOptions: TransformOptions,
graph: Graph,
delta: Delta,
onProgress?: (numProcessed: number, total: number) => mixed = () => {},
options: Options,
): Promise<void> {
let numProcessed = 0;
let total = 1;
onProgress(numProcessed, total);
options.onProgress && options.onProgress(numProcessed, total);
await processEdge(
edge,
dependencyGraph,
transformOptions,
graph,
delta,
options,
() => {
total++;
onProgress(numProcessed, total);
options.onProgress && options.onProgress(numProcessed, total);
},
() => {
numProcessed++;
onProgress(numProcessed, total);
options.onProgress && options.onProgress(numProcessed, total);
},
);
numProcessed++;
onProgress(numProcessed, total);
options.onProgress && options.onProgress(numProcessed, total);
}
async function processEdge(
edge: DependencyEdge,
dependencyGraph: DependencyGraph,
transformOptions: TransformOptions,
graph: Graph,
delta: Delta,
options: Options,
onDependencyAdd: () => mixed,
onDependencyAdded: () => mixed,
): Promise<void> {
const previousDependencies = edge.dependencies;
const {type, ...workerTransformOptions} = transformOptions;
const module = dependencyGraph.getModuleForPath(edge.path, type === 'script');
const result = await module.read(
removeInlineRequiresBlacklistFromOptions(edge.path, workerTransformOptions),
);
const result = await options.transform(edge.path);
// Get the absolute path of all sub-dependencies (some of them could have been
// moved but maintain the same relative path).
const currentDependencies = resolveDependencies(
edge.path,
result.dependencies,
dependencyGraph,
transformOptions,
options,
);
// Update the edge information.
edge.output.code = result.code;
edge.output.map = result.map;
// TODO(T28259615): Remove as soon as possible to avoid leaking.
// Lazily access source code; if not needed, don't read the file.
// eslint-disable-next-line lint/flow-no-fixme
// $FlowFixMe: "defineProperty" with a getter is buggy in flow.
Object.defineProperty(edge.output, 'source', {
configurable: true,
enumerable: true,
get: () => result.source,
});
edge.output = result.output;
edge.dependencies = new Map();
currentDependencies.forEach((absolutePath, relativePath) => {
@ -266,10 +227,9 @@ async function processEdge(
await addDependency(
edge,
absolutePath,
dependencyGraph,
transformOptions,
graph,
delta,
options,
onDependencyAdd,
onDependencyAdded,
);
@ -281,10 +241,9 @@ async function processEdge(
async function addDependency(
parentEdge: DependencyEdge,
path: string,
dependencyGraph: DependencyGraph,
transformOptions: TransformOptions,
graph: Graph,
delta: Delta,
options: Options,
onDependencyAdd: () => mixed,
onDependencyAdded: () => mixed,
): Promise<void> {
@ -297,10 +256,7 @@ async function addDependency(
return;
}
const edge = createEdge(
dependencyGraph.getModuleForPath(path, transformOptions.type === 'script'),
graph,
);
const edge = createEdge(path, graph);
edge.inverseDependencies.add(parentEdge.path);
delta.added.set(edge.path, edge);
@ -309,10 +265,9 @@ async function addDependency(
await processEdge(
edge,
dependencyGraph,
transformOptions,
graph,
delta,
options,
onDependencyAdd,
onDependencyAdded,
);
@ -353,58 +308,36 @@ function removeDependency(
destroyEdge(edge, graph);
}
function createEdge(module: Module, graph: Graph): DependencyEdge {
function createEdge(filePath: string, graph: Graph): DependencyEdge {
const edge = {
dependencies: new Map(),
inverseDependencies: new Set(),
path: module.path,
path: filePath,
output: {
code: '',
map: [],
source: '',
type: getType(module),
type: 'module',
},
};
graph.dependencies.set(module.path, edge);
graph.dependencies.set(filePath, edge);
return edge;
}
function getType(module: Module): DependencyType {
if (module.isAsset()) {
return 'asset';
}
if (module.isPolyfill()) {
return 'script';
}
return 'module';
}
function destroyEdge(edge: DependencyEdge, graph: Graph) {
graph.dependencies.delete(edge.path);
}
function resolveDependencies(
parentPath,
parentPath: string,
dependencies: $ReadOnlyArray<string>,
dependencyGraph: DependencyGraph,
transformOptions: TransformOptions,
options: Options,
): Map<string, string> {
const parentModule = dependencyGraph.getModuleForPath(
parentPath,
transformOptions.type === 'string',
);
return new Map(
dependencies.map(relativePath => [
relativePath,
dependencyGraph.resolveDependency(
parentModule,
relativePath,
transformOptions.platform,
).path,
options.resolve(parentPath, relativePath),
]),
);
}

View File

@ -57,29 +57,30 @@ class HmrServer<TClient: Client> {
const {bundleEntry, platform} = nullthrows(urlObj.query);
const customTransformOptions = parseCustomTransformOptions(urlObj);
// Create a new DeltaTransformer for each client. Once the clients are
// Create a new graph for each client. Once the clients are
// modified to support Delta Bundles, they'll be able to pass the
// DeltaBundleId param through the WS connection and we'll be able to share
// the same DeltaTransformer between the WS connection and the HTTP one.
const deltaBundler = this._packagerServer.getDeltaBundler();
const graph = await deltaBundler.buildGraph({
assetPlugins: [],
customTransformOptions,
dev: true,
entryPoints: [
getAbsolutePath(bundleEntry, this._packagerServer.getProjectRoots()),
],
hot: true,
minify: false,
onProgress: null,
platform,
type: 'module',
});
// the same graph between the WS connection and the HTTP one.
const graph = await this._packagerServer.buildGraph(
[getAbsolutePath(bundleEntry, this._packagerServer.getProjectRoots())],
{
assetPlugins: [],
customTransformOptions,
dev: true,
hot: true,
minify: false,
onProgress: null,
platform,
type: 'module',
},
);
// Listen to file changes.
const client = {sendFn, graph};
deltaBundler.listen(graph, this._handleFileChange.bind(this, client));
this._packagerServer
.getDeltaBundler()
.listen(graph, this._handleFileChange.bind(this, client));
return client;
}

View File

@ -32,12 +32,12 @@ describe('HmrServer', () => {
callbacks = new Map();
deltaBundlerMock = {
buildGraph: buildGraphMock,
listen: (graph, cb) => {
callbacks.set(graph, cb);
},
};
serverMock = {
buildGraph: buildGraphMock,
getDeltaBundler() {
return deltaBundlerMock;
},
@ -66,9 +66,9 @@ describe('HmrServer', () => {
);
expect(buildGraphMock).toBeCalledWith(
['/root/EntryPoint.js'],
expect.objectContaining({
dev: true,
entryPoints: ['/root/EntryPoint.js'],
minify: false,
platform: 'ios',
}),

View File

@ -36,6 +36,7 @@ const parseCustomTransformOptions = require('./lib/parseCustomTransformOptions')
const parsePlatformFilePath = require('./node-haste/lib/parsePlatformFilePath');
const path = require('path');
const symbolicate = require('./Server/symbolicate/symbolicate');
const transformHelpers = require('./lib/transformHelpers');
const url = require('url');
const {getAsset} = require('./Assets');
@ -77,11 +78,10 @@ type GraphInfo = {|
+sequenceId: string,
|};
type BuildGraphOptions = {|
export type BuildGraphOptions = {|
+assetPlugins: Array<string>,
+customTransformOptions: CustomTransformOptions,
+dev: boolean,
+entryFiles: $ReadOnlyArray<string>,
+hot: boolean,
+minify: boolean,
+onProgress: ?(doneCont: number, totalCount: number) => mixed,
@ -289,19 +289,26 @@ class Server {
};
}
async buildGraph(options: BuildGraphOptions): Promise<Graph> {
return await this._deltaBundler.buildGraph({
assetPlugins: options.assetPlugins,
customTransformOptions: options.customTransformOptions,
dev: options.dev,
entryPoints: options.entryFiles.map(entryFile =>
getAbsolutePath(entryFile, this._opts.projectRoots),
async buildGraph(
entryFiles: $ReadOnlyArray<string>,
options: BuildGraphOptions,
): Promise<Graph> {
entryFiles = entryFiles.map(entryFile =>
getAbsolutePath(entryFile, this._opts.projectRoots),
);
return await this._deltaBundler.buildGraph(entryFiles, {
resolve: await transformHelpers.getResolveDependencyFn(
this._bundler,
options.platform,
),
transform: await transformHelpers.getTransformFn(
entryFiles,
this._bundler,
this._deltaBundler,
options,
),
hot: options.hot,
minify: options.minify,
onProgress: options.onProgress,
platform: options.platform,
type: options.type,
});
}
@ -376,7 +383,6 @@ class Server {
assetPlugins: options.assetPlugins,
customTransformOptions: options.customTransformOptions,
dev: options.dev,
entryPoints: [entryPoint],
hot: options.hot,
minify: options.minify,
onProgress: options.onProgress,
@ -384,10 +390,24 @@ class Server {
type: 'module',
};
const graph = await this._deltaBundler.buildGraph(crawlingOptions);
const graph = await this._deltaBundler.buildGraph([entryPoint], {
resolve: await transformHelpers.getResolveDependencyFn(
this._bundler,
options.platform,
),
transform: await transformHelpers.getTransformFn(
[entryPoint],
this._bundler,
this._deltaBundler,
crawlingOptions,
),
onProgress: options.onProgress,
});
const prepend = await getPrependedScripts(
this._opts,
crawlingOptions,
this._bundler,
this._deltaBundler,
);

View File

@ -24,6 +24,7 @@ jest
.mock('metro-core/src/Logger')
.mock('../../lib/getAbsolutePath')
.mock('../../lib/getPrependedScripts')
.mock('../../lib/transformHelpers')
.mock('../../lib/GlobalTransformCache');
const NativeDate = global.Date;
@ -35,6 +36,7 @@ describe('processRequest', () => {
let dependencies;
let getAsset;
let getPrependedScripts;
let transformHelpers;
let symbolicate;
let DeltaBundler;
@ -49,6 +51,7 @@ describe('processRequest', () => {
crypto = require('crypto');
getAsset = require('../../Assets').getAsset;
getPrependedScripts = require('../../lib/getPrependedScripts');
transformHelpers = require('../../lib/transformHelpers');
symbolicate = require('../symbolicate/symbolicate');
DeltaBundler = require('../../DeltaBundler');
});
@ -176,6 +179,11 @@ describe('processRequest', () => {
server = new Server(options);
requestHandler = server.processRequest.bind(server);
transformHelpers.getTransformFn = jest.fn().mockReturnValue(() => {});
transformHelpers.getResolveDependencyFn = jest
.fn()
.mockReturnValue(() => {});
let i = 0;
crypto.randomBytes.mockImplementation(() => `XXXXX-${i++}`);
});
@ -369,73 +377,59 @@ describe('processRequest', () => {
expect(DeltaBundler.prototype.buildGraph.mock.calls.length).toBe(1);
});
it('works with .ios.js extension', () => {
return makeRequest(requestHandler, 'index.ios.includeRequire.bundle').then(
response => {
expect(DeltaBundler.prototype.buildGraph).toBeCalledWith({
assetPlugins: [],
customTransformOptions: {},
dev: true,
entryPoints: ['/root/index.ios.js'],
hot: true,
minify: false,
onProgress: jasmine.any(Function),
platform: null,
type: 'module',
});
},
);
});
it('passes in the platform param', async () => {
await makeRequest(requestHandler, 'index.bundle?platform=ios');
it('passes in the platform param', function() {
return makeRequest(requestHandler, 'index.bundle?platform=ios').then(
function(response) {
expect(DeltaBundler.prototype.buildGraph).toBeCalledWith({
assetPlugins: [],
customTransformOptions: {},
dev: true,
entryPoints: ['/root/index.js'],
hot: true,
minify: false,
onProgress: jasmine.any(Function),
platform: 'ios',
type: 'module',
});
},
);
});
it('passes in the assetPlugin param', function() {
return makeRequest(
requestHandler,
'index.bundle?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2',
).then(function(response) {
expect(DeltaBundler.prototype.buildGraph).toBeCalledWith({
assetPlugins: ['assetPlugin1', 'assetPlugin2'],
expect(transformHelpers.getTransformFn).toBeCalledWith(
['/root/index.js'],
jasmine.any(Bundler),
jasmine.any(DeltaBundler),
{
assetPlugins: [],
customTransformOptions: {},
dev: true,
entryPoints: ['/root/index.js'],
hot: true,
minify: false,
onProgress: jasmine.any(Function),
platform: null,
platform: 'ios',
type: 'module',
});
});
},
);
expect(transformHelpers.getResolveDependencyFn).toBeCalled();
expect(DeltaBundler.prototype.buildGraph).toBeCalledWith(
['/root/index.js'],
{
resolve: jasmine.any(Function),
transform: jasmine.any(Function),
onProgress: jasmine.any(Function),
},
);
});
it('passes in the assetPlugin param', async () => {
await makeRequest(
requestHandler,
'index.bundle?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2',
);
expect(transformHelpers.getTransformFn).toBeCalledWith(
['/root/index.js'],
jasmine.any(Bundler),
jasmine.any(DeltaBundler),
jasmine.objectContaining({
assetPlugins: ['assetPlugin1', 'assetPlugin2'],
}),
);
});
it('does not rebuild the bundle when making concurrent requests', async () => {
let resolveBuildGraph;
// Delay the response of the buildGraph method.
DeltaBundler.prototype.buildGraph.mockImplementation(async () => {
transformHelpers.getResolveDependencyFn.mockImplementation(async () => {
return new Promise(res => (resolveBuildGraph = res));
});
DeltaBundler.prototype.getDelta.mockReturnValue({
modified: new Map(),
deleted: new Set(),
reset: false,
});
const promise1 = makeRequest(requestHandler, 'index.bundle');
const promise2 = makeRequest(requestHandler, 'index.bundle');
@ -589,17 +583,9 @@ describe('processRequest', () => {
it('does return the same initial delta when making concurrent requests', async () => {
let resolveBuildGraph;
// force the buildGraph
DeltaBundler.prototype.buildGraph.mockImplementation(async () => {
transformHelpers.getResolveDependencyFn.mockImplementation(async () => {
return new Promise(res => (resolveBuildGraph = res));
});
DeltaBundler.prototype.getDelta.mockImplementation(
async (graph, {reset}) => ({
modified: reset ? dependencies : new Map(),
deleted: new Set(),
reset,
}),
);
const promise1 = makeRequest(requestHandler, 'index.delta');
const promise2 = makeRequest(requestHandler, 'index.delta');
@ -754,25 +740,37 @@ describe('processRequest', () => {
});
describe('build(options)', () => {
it('Calls the delta bundler with the correct args', () => {
return server
.build({
...Server.DEFAULT_BUNDLE_OPTIONS,
entryFile: 'foo file',
})
.then(() =>
expect(DeltaBundler.prototype.buildGraph).toBeCalledWith({
assetPlugins: [],
customTransformOptions: {},
dev: true,
entryPoints: ['/root/foo file'],
hot: false,
minify: false,
onProgress: null,
platform: undefined,
type: 'module',
}),
);
it('Calls the delta bundler with the correct args', async () => {
await server.build({
...Server.DEFAULT_BUNDLE_OPTIONS,
entryFile: 'foo file',
});
expect(transformHelpers.getTransformFn).toBeCalledWith(
['/root/foo file'],
jasmine.any(Bundler),
jasmine.any(DeltaBundler),
{
assetPlugins: [],
customTransformOptions: {},
dev: true,
hot: false,
minify: false,
onProgress: null,
platform: undefined,
type: 'module',
},
);
expect(transformHelpers.getResolveDependencyFn).toBeCalled();
expect(DeltaBundler.prototype.buildGraph).toBeCalledWith(
['/root/foo file'],
{
resolve: jasmine.any(Function),
transform: jasmine.any(Function),
onProgress: null,
},
);
});
});

View File

@ -10,7 +10,7 @@
'use strict';
const DeltaCalculator = require('../DeltaBundler/DeltaCalculator');
const transformHelpers = require('../lib/transformHelpers');
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
@ -28,31 +28,21 @@ async function getTransformOptions(): Promise<JSTransformerOptions> {
};
},
};
const dependencyGraph = {
getWatcher() {
return {on() {}};
},
getAbsolutePath(path) {
return '/' + path;
},
};
const options = {
assetPlugins: [],
dev: true,
entryPoints: [],
hot: true,
minify: false,
platform: 'ios',
type: 'module',
};
const deltaCalculator = new DeltaCalculator(
return await transformHelpers.calcTransformerOptions(
[],
bundler,
dependencyGraph,
options,
{},
{
assetPlugins: [],
dev: true,
entryPoints: [],
hot: true,
minify: false,
platform: 'ios',
type: 'module',
},
);
return await deltaCalculator.getTransformerOptions();
}
module.exports = getTransformOptions;

View File

@ -388,10 +388,9 @@ exports.buildGraph = async function({
});
try {
return await metroServer.buildGraph({
return await metroServer.buildGraph(entries, {
...MetroServer.DEFAULT_GRAPH_OPTIONS,
dev,
entryFiles: entries,
onProgress,
platform,
});

View File

@ -1,55 +0,0 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @emails oncall+js_foundation
*/
'use strict';
const removeInlineRequiresBlacklistFromOptions = require('../removeInlineRequiresBlacklistFromOptions');
it('should not touch a transformOption object with boolean inlineRequires', () => {
const transformOptions = {
inlineRequires: false,
};
expect(
removeInlineRequiresBlacklistFromOptions('/path', transformOptions),
).toBe(transformOptions);
});
it('should change inlineRequires to true when the path is not in the blacklist', () => {
const transformOptions = {
inlineRequires: {
blacklist: {'/other': true},
},
foo: 'bar',
};
expect(
removeInlineRequiresBlacklistFromOptions('/path', transformOptions),
).toEqual({
inlineRequires: true,
foo: 'bar',
});
});
it('should change inlineRequires to false when the path is in the blacklist', () => {
const transformOptions = {
inlineRequires: {
blacklist: {'/path': true},
},
foo: 'bar',
};
expect(
removeInlineRequiresBlacklistFromOptions('/path', transformOptions),
).toEqual({
inlineRequires: false,
foo: 'bar',
});
});

View File

@ -12,7 +12,9 @@
const defaults = require('../defaults');
const getPreludeCode = require('./getPreludeCode');
const transformHelpers = require('./transformHelpers');
import type Bundler from '../Bundler';
import type {DependencyEdge} from '../DeltaBundler/traverseDependencies';
import type DeltaBundler from '../DeltaBundler';
import type {CustomTransformOptions} from '../JSTransformer/worker';
@ -33,6 +35,7 @@ type BundleOptions = {
async function getPrependedScripts(
options: Options,
bundleOptions: BundleOptions,
bundler: Bundler,
deltaBundler: DeltaBundler,
): Promise<Array<DependencyEdge>> {
// Get all the polyfills from the relevant option params (the
@ -43,17 +46,33 @@ async function getPrependedScripts(
})
.concat(options.polyfillModuleNames);
const graph = await deltaBundler.buildGraph({
const buildOptions = {
assetPlugins: [],
customTransformOptions: bundleOptions.customTransformOptions,
dev: bundleOptions.dev,
entryPoints: [defaults.moduleSystem, ...polyfillModuleNames],
hot: bundleOptions.hot,
minify: bundleOptions.minify,
onProgress: null,
platform: bundleOptions.platform,
type: 'script',
});
};
const graph = await deltaBundler.buildGraph(
[defaults.moduleSystem, ...polyfillModuleNames],
{
resolve: await transformHelpers.getResolveDependencyFn(
bundler,
buildOptions.platform,
),
transform: await transformHelpers.getTransformFn(
[defaults.moduleSystem, ...polyfillModuleNames],
bundler,
deltaBundler,
buildOptions,
),
onProgress: null,
},
);
return [
_getPrelude({dev: bundleOptions.dev}),

View File

@ -1,29 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
function removeInlineRequiresBlacklistFromOptions(
path: string,
transformOptions: JSTransformerOptions,
): JSTransformerOptions {
if (typeof transformOptions.inlineRequires === 'object') {
return {
...transformOptions,
inlineRequires: !(path in transformOptions.inlineRequires.blacklist),
};
}
return transformOptions;
}
module.exports = removeInlineRequiresBlacklistFromOptions;

View File

@ -0,0 +1,163 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type Bundler from '../Bundler';
import type {TransformFn} from '../DeltaBundler/traverseDependencies';
import type DeltaBundler from '../DeltaBundler';
import type {TransformOptions} from '../JSTransformer/worker';
import type {BuildGraphOptions} from '../Server';
type InlineRequiresRaw = {+blacklist: {[string]: true}} | boolean;
async function calcTransformerOptions(
entryFiles: $ReadOnlyArray<string>,
bundler: Bundler,
deltaBundler: DeltaBundler,
options: BuildGraphOptions,
): Promise<{...TransformOptions, inlineRequires: InlineRequiresRaw}> {
const {
enableBabelRCLookup,
projectRoot,
} = bundler.getGlobalTransformOptions();
const transformOptionsForBlacklist = {
assetDataPlugins: options.assetPlugins,
customTransformOptions: options.customTransformOptions,
enableBabelRCLookup,
dev: options.dev,
hot: options.hot,
inlineRequires: false,
minify: options.minify,
platform: options.platform,
projectRoot,
};
// When we're processing scripts, we don't need to calculate any
// inlineRequires information, since scripts by definition don't have
// requires().
if (options.type === 'script') {
return {
...transformOptionsForBlacklist,
inlineRequires: false,
};
}
const {inlineRequires} = await bundler.getTransformOptionsForEntryFiles(
entryFiles,
{dev: options.dev, platform: options.platform},
async path => {
const {dependencies} = await deltaBundler.buildGraph([path], {
resolve: await getResolveDependencyFn(bundler, options.platform),
transform: await getTransformFn([path], bundler, deltaBundler, options),
onProgress: null,
});
return Array.from(dependencies.keys());
},
);
return {
...transformOptionsForBlacklist,
inlineRequires: inlineRequires || false,
};
}
function removeInlineRequiresBlacklistFromOptions(
path: string,
inlineRequires: InlineRequiresRaw,
): boolean {
if (typeof inlineRequires === 'object') {
return !(path in inlineRequires.blacklist);
}
return inlineRequires;
}
async function getTransformFn(
entryFiles: $ReadOnlyArray<string>,
bundler: Bundler,
deltaBundler: DeltaBundler,
options: BuildGraphOptions,
): Promise<TransformFn> {
const dependencyGraph = await bundler.getDependencyGraph();
const {inlineRequires, ...transformerOptions} = await calcTransformerOptions(
entryFiles,
bundler,
deltaBundler,
options,
);
return async (path: string) => {
const module = dependencyGraph.getModuleForPath(
path,
options.type === 'script',
);
const result = await module.read({
...transformerOptions,
inlineRequires: removeInlineRequiresBlacklistFromOptions(
path,
inlineRequires,
),
});
let type = 'module';
if (module.isAsset()) {
type = 'asset';
}
if (module.isPolyfill()) {
type = 'script';
}
// eslint-disable-next-line lint/flow-no-fixme
// $FlowFixMe: "defineProperty" with a getter is buggy in flow.
const output = {
code: result.code,
map: result.map,
type,
};
// Lazily access source code; if not needed, don't read the file.
// eslint-disable-next-line lint/flow-no-fixme
// $FlowFixMe: "defineProperty" with a getter is buggy in flow.
Object.defineProperty(output, 'source', {
configurable: true,
enumerable: true,
get: () => result.source,
});
return {
output,
dependencies: result.dependencies,
};
};
}
async function getResolveDependencyFn(
bundler: Bundler,
platform: ?string,
): Promise<(from: string, to: string) => string> {
const dependencyGraph = await bundler.getDependencyGraph();
return (from: string, to: string) => {
return dependencyGraph.resolveDependency(
dependencyGraph.getModuleForPath(from, false),
to,
platform,
).path;
};
}
module.exports = {
calcTransformerOptions,
getTransformFn,
getResolveDependencyFn,
};