Skip to content

Commit

Permalink
Use explicit key for tracking dependencies in bundler (#835)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #835

Changes `collectDependencies` so it is responsible for providing a key with each extracted dependency. This key should be *stable* across builds and when dependencies are reordered, and *must* be locally unique within a given module.

NOTE: This is a similar concept to the [`key`](https://reactjs.org/docs/lists-and-keys.html#keys) prop in React. It's also used for a similar purpose - `traverseDependencies` is not unlike a tree diffing algorithm.

Previously, the `name` property ( = the first argument to `require` etc) *implicitly* served as this key in both `collectDependencies` and DeltaBundler, but since the addition of `require.context` dependency descriptors in #821, this is no longer strictly correct. (This diff therefore unblocks implementing `require.context` in DeltaBundler.)

Instead of teaching DeltaBundler to internally re-derive suitable keys from the dependency descriptors - essentially duplicating the logic in `collectDependencies` - the approach taken here is to require `collectDependencies` to return an *explicit* key as part of each descriptor.

NOTE: Keys should be considered completely opaque. As an implementation detail (that may change without notice), we hash the keys to obfuscate them and deter downstream code from depending on the information in them.

Note that it's safe for multiple descriptors to resolve to a single module (as of D37194640 (fc29a11)), so from DeltaBundler's perspective it's now perfectly valid for `collectDependencies` to not collapse dependencies at all (e.g. even generate one descriptor per `require` call site) as long as it provides a unique key with each one.

WARNING: This diff exposes a **preexisting** bug affecting optional dependencies (introduced in #511) - see FIXME comment in `graphOperations.js` for the details. This will require more followup in a separate diff.

Changelog: [Internal]

Reviewed By: jacdebug

Differential Revision: D37205825

fbshipit-source-id: 92dc306803e647b25bd576dae02960215fc01da6
  • Loading branch information
motiz88 authored and facebook-github-bot committed Jul 18, 2022
1 parent 7f786ba commit 52e1a00
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ beforeEach(() => {
'bar',
{
absolutePath: '/bar',
data: {data: {asyncType: null, locs: []}, name: 'bar'},
data: {data: {asyncType: null, locs: [], key: 'bar'}, name: 'bar'},
},
],
[
'baz',
{
absolutePath: '/baz',
data: {data: {asyncType: null, locs: []}, name: 'baz'},
data: {data: {asyncType: null, locs: [], key: 'baz'}, name: 'baz'},
},
],
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ beforeEach(() => {
'bar',
{
absolutePath: '/bar',
data: {data: {asyncType: null, locs: []}, name: 'bar'},
data: {data: {asyncType: null, locs: [], key: 'bar'}, name: 'bar'},
},
],
[
'baz',
{
absolutePath: '/baz',
data: {data: {asyncType: null, locs: []}, name: 'baz'},
data: {data: {asyncType: null, locs: [], key: 'baz'}, name: 'baz'},
},
],
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ Object {
"dependencies": Map {
"/bundle" => Object {
"dependencies": Map {
"foo" => Object {
"C+7Hteo/D9vJXQ3UfzxbwnXaijM=" => Object {
"absolutePath": "/foo",
"data": Object {
"data": Object {
"asyncType": null,
"key": "C+7Hteo/D9vJXQ3UfzxbwnXaijM=",
"locs": Array [],
},
"name": "foo",
Expand All @@ -32,21 +33,23 @@ Object {
},
"/foo" => Object {
"dependencies": Map {
"bar" => Object {
"Ys23Ag/5IOWqZCw9QGaVDdHwH00=" => Object {
"absolutePath": "/bar",
"data": Object {
"data": Object {
"asyncType": null,
"key": "Ys23Ag/5IOWqZCw9QGaVDdHwH00=",
"locs": Array [],
},
"name": "bar",
},
},
"baz" => Object {
"u+lgol6jEdIdQGaek98gA7qbkKI=" => Object {
"absolutePath": "/baz",
"data": Object {
"data": Object {
"asyncType": null,
"key": "u+lgol6jEdIdQGaek98gA7qbkKI=",
"locs": Array [],
},
"name": "baz",
Expand Down Expand Up @@ -127,11 +130,12 @@ Object {
"dependencies": Map {
"/bundle" => Object {
"dependencies": Map {
"foo" => Object {
"C+7Hteo/D9vJXQ3UfzxbwnXaijM=" => Object {
"absolutePath": "/foo",
"data": Object {
"data": Object {
"asyncType": null,
"key": "C+7Hteo/D9vJXQ3UfzxbwnXaijM=",
"locs": Array [],
},
"name": "foo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const {
traverseDependencies: traverseDependenciesImpl,
} = require('../graphOperations');

const {objectContaining} = expect;

type DependencyDataInput = $Shape<TransformResultDependency['data']>;

let mockedDependencies: Set<string> = new Set();
Expand All @@ -39,7 +41,7 @@ let mockedDependencyTree: Map<
$ReadOnly<{
name: string,
path: string,
data?: DependencyDataInput,
data: DependencyDataInput,
}>,
>,
> = new Map();
Expand Down Expand Up @@ -88,11 +90,22 @@ const Actions = {
data?: DependencyDataInput,
) {
const deps = nullthrows(mockedDependencyTree.get(path));
const depName = name ?? dependencyPath.replace('/', '');
const key = require('crypto')
.createHash('sha1')
.update(depName)
.digest('base64');
const dep = {
name: name ?? dependencyPath.replace('/', ''),
name: depName,
path: dependencyPath,
data: data ?? {},
data: {key, ...(data ?? {})},
};
if (
deps.findIndex(existingDep => existingDep.data.key === dep.data.key) !==
-1
) {
throw new Error('Found existing mock dep with key: ' + dep.data.key);
}
if (position == null) {
deps.push(dep);
} else {
Expand Down Expand Up @@ -273,6 +286,7 @@ beforeEach(async () => {
data: {
asyncType: null,
locs: [],
key: dep.data.key,
...dep.data,
},
})),
Expand Down Expand Up @@ -839,24 +853,33 @@ describe('edge cases', () => {
...nullthrows(graph.dependencies.get(moduleFoo)).dependencies,
]).toEqual([
[
'qux',
expect.any(String),
{
absolutePath: '/qux',
data: {data: {asyncType: null, locs: []}, name: 'qux'},
data: {
data: objectContaining({asyncType: null, locs: []}),
name: 'qux',
},
},
],
[
'bar',
expect.any(String),
{
absolutePath: '/bar',
data: {data: {asyncType: null, locs: []}, name: 'bar'},
data: {
data: objectContaining({asyncType: null, locs: []}),
name: 'bar',
},
},
],
[
'baz',
expect.any(String),
{
absolutePath: '/baz',
data: {data: {asyncType: null, locs: []}, name: 'baz'},
data: {
data: objectContaining({asyncType: null, locs: []}),
name: 'baz',
},
},
],
]);
Expand Down Expand Up @@ -1953,21 +1976,21 @@ describe('edge cases', () => {
...nullthrows(graph.dependencies.get(entryModule)).dependencies,
]).toEqual([
[
'foo.js',
expect.any(String),
{
absolutePath: '/foo',
data: {
data: {asyncType: null, locs: []},
data: objectContaining({asyncType: null, locs: []}),
name: 'foo.js',
},
},
],
[
'foo',
expect.any(String),
{
absolutePath: '/foo',
data: {
data: {asyncType: null, locs: []},
data: objectContaining({asyncType: null, locs: []}),
name: 'foo',
},
},
Expand Down Expand Up @@ -2059,12 +2082,12 @@ describe('edge cases', () => {
[
entryModule,
[
{name: 'foo', path: moduleFoo},
{name: 'bar', path: moduleBar},
{name: 'foo', path: moduleFoo, data: {key: 'foo'}},
{name: 'bar', path: moduleBar, data: {key: 'bar'}},
],
],
[moduleFoo, [{name: 'baz', path: moduleBaz}]],
[moduleBar, [{name: 'baz', path: moduleBaz}]],
[moduleFoo, [{name: 'baz', path: moduleBaz, data: {key: 'baz'}}]],
[moduleBar, [{name: 'baz', path: moduleBaz, data: {key: 'baz'}}]],
]);

// Test that even when having different modules taking longer, the order
Expand All @@ -2091,6 +2114,7 @@ describe('reorderGraph', () => {
data: {
asyncType: null,
locs: [],
key: path.substr(1),
},
name: path.substr(1),
},
Expand Down Expand Up @@ -2231,17 +2255,14 @@ describe('optional dependencies', () => {
});

describe('parallel edges', () => {
it('add twice w/ same key, build and remove once', async () => {
it('add twice w/ same name, build and remove once', async () => {
// Create a second edge between /foo and /bar.
Actions.addDependency('/foo', '/bar', undefined);
Actions.addDependency('/foo', '/bar', undefined, undefined, {
key: 'bar-second-key',
});

await initialTraverseDependencies(graph, options);

const fooDeps = nullthrows(graph.dependencies.get('/foo')).dependencies;
const fooDepsResolved = [...fooDeps.values()].map(dep => dep.absolutePath);
// We dedupe the dependencies because they have the same `name`.
expect(fooDepsResolved).toEqual(['/bar', '/baz']);

// Remove one of the edges between /foo and /bar (arbitrarily)
Actions.removeDependency('/foo', '/bar');

Expand All @@ -2254,17 +2275,14 @@ describe('parallel edges', () => {
});
});

it('add twice w/ same key, build and remove twice', async () => {
it('add twice w/ same name, build and remove twice', async () => {
// Create a second edge between /foo and /bar.
Actions.addDependency('/foo', '/bar', undefined);
Actions.addDependency('/foo', '/bar', undefined, undefined, {
key: 'bar-second-key',
});

await initialTraverseDependencies(graph, options);

const fooDeps = nullthrows(graph.dependencies.get('/foo')).dependencies;
const fooDepsResolved = [...fooDeps.values()].map(dep => dep.absolutePath);
// We dedupe the dependencies because they have the same `name`.
expect(fooDepsResolved).toEqual(['/bar', '/baz']);

// Remove both edges between /foo and /bar
Actions.removeDependency('/foo', '/bar');
Actions.removeDependency('/foo', '/bar');
Expand All @@ -2278,17 +2296,12 @@ describe('parallel edges', () => {
});
});

it('add twice w/ different keys, build and remove once', async () => {
it('add twice w/ different names, build and remove once', async () => {
// Create a second edge between /foo and /bar, with a different `name`.
Actions.addDependency('/foo', '/bar', undefined, 'bar-second');

await initialTraverseDependencies(graph, options);

const fooDeps = nullthrows(graph.dependencies.get('/foo')).dependencies;
const fooDepsResolved = [...fooDeps.values()].map(dep => dep.absolutePath);
// We don't dedupe the dependencies because they have different `name`s.
expect(fooDepsResolved).toEqual(['/bar', '/baz', '/bar']);

// Remove one of the edges between /foo and /bar (arbitrarily)
Actions.removeDependency('/foo', '/bar');

Expand All @@ -2301,17 +2314,12 @@ describe('parallel edges', () => {
});
});

it('add twice w/ different keys, build and remove twice', async () => {
it('add twice w/ different names, build and remove twice', async () => {
// Create a second edge between /foo and /bar, with a different `name`.
Actions.addDependency('/foo', '/bar', undefined, 'bar-second');

await initialTraverseDependencies(graph, options);

const fooDeps = nullthrows(graph.dependencies.get('/foo')).dependencies;
const fooDepsResolved = [...fooDeps.values()].map(dep => dep.absolutePath);
// We don't dedupe the dependencies because they have different `name`s.
expect(fooDepsResolved).toEqual(['/bar', '/baz', '/bar']);

// Remove both edges between /foo and /bar
Actions.removeDependency('/foo', '/bar');
Actions.removeDependency('/foo', '/bar');
Expand Down

0 comments on commit 52e1a00

Please sign in to comment.