DevTools: Parse named source AST in a worker (#21902)
Resolves #21855 Ended up using workerize in order to setup the worker once it allows easy imports (for babel's parse function) and exports.
Lucas Correia committed
Jul 21, 2021 at 13:16 UTC
25f09e3e4e900fc5d510243ce9ed3272d0af59e7
9 files changed
+161
-45
packages/react-devtools-extensions/firefox/manifest.json
+1
-1
@@ -32,7 +32,7 @@
32
33
"devtools_page": "main.html",
34
35
- "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
35
+ "content_security_policy": "script-src 'self' 'unsafe-eval' blob:; object-src 'self'",
36
"web_accessible_resources": [
37
"main.html",
38
"panel.html",
packages/react-devtools-extensions/package.json
+3
-2
@@ -19,7 +19,6 @@
19
"update-mock-source-maps": "node ./src/__tests__/updateMockSourceMaps.js"
20
},
21
"devDependencies": {
22
- "acorn-jsx": "^5.2.0",
22
"@babel/core": "^7.11.1",
23
"@babel/plugin-proposal-class-properties": "^7.10.4",
24
"@babel/plugin-transform-flow-strip-types": "^7.10.4",
@@ -28,6 +27,7 @@
27
"@babel/preset-env": "^7.11.0",
28
"@babel/preset-flow": "^7.10.4",
29
"@babel/preset-react": "^7.10.4",
30
+ "acorn-jsx": "^5.2.0",
31
"archiver": "^3.0.0",
32
"babel-core": "^7.0.0-bridge",
33
"babel-eslint": "^9.0.0",
@@ -55,7 +55,8 @@
55
"web-ext": "^3.0.0",
56
"webpack": "^4.43.0",
57
"webpack-cli": "^3.3.11",
58
- "webpack-dev-server": "^3.10.3"
58
+ "webpack-dev-server": "^3.10.3",
59
+ "workerize-loader": "^1.3.0"
60
},
61
"dependencies": {
62
"web-ext": "^4"
packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js
+99
-32
@@ -11,6 +11,37 @@
11
// This is done to control if and how the code is transformed at runtime.
12
// Do not declare test components within this test file as it is very fragile.
13
14
+function expectHookNamesToEqual(map, expectedNamesArray) {
15
+ // Slightly hacky since it relies on the iterable order of values()
16
+ expect(Array.from(map.values())).toEqual(expectedNamesArray);
17
+}
18
+
19
+function requireText(path, encoding) {
20
+ const {existsSync, readFileSync} = require('fs');
21
+ if (existsSync(path)) {
22
+ return Promise.resolve(readFileSync(path, encoding));
23
+ } else {
24
+ return Promise.reject(`File not found "${path}"`);
25
+ }
26
+}
27
+
28
+const chromeGlobal = {
29
+ extension: {
30
+ getURL: jest.fn((...args) => {
31
+ const {join} = require('path');
32
+ return join(
33
+ __dirname,
34
+ '..',
35
+ '..',
36
+ 'node_modules',
37
+ 'source-map',
38
+ 'lib',
39
+ 'mappings.wasm',
40
+ );
41
+ }),
42
+ },
43
+};
44
+
45
describe('parseHookNames', () => {
46
let fetchMock;
47
let inspectHooks;
@@ -26,6 +57,9 @@ describe('parseHookNames', () => {
57
fetchMock = require('jest-fetch-mock');
58
fetchMock.enableMocks();
59
60
+ // Mock out portion of browser API used by parseHookNames to initialize "source-map".
61
+ global.chrome = chromeGlobal;
62
+
63
inspectHooks = require('react-debug-tools/src/ReactDebugHooks')
64
.inspectHooks;
65
parseHookNames = require('../parseHookNames').parseHookNames;
@@ -45,44 +79,12 @@ describe('parseHookNames', () => {
79
fetchMock.mockIf(/.+$/, request => {
80
return requireText(request.url, 'utf8');
81
});
48
-
49
- // Mock out portion of browser API used by parseHookNames to initialize "source-map".
50
- global.chrome = {
51
- extension: {
52
- getURL: jest.fn((...args) => {
53
- const {join} = require('path');
54
- return join(
55
- __dirname,
56
- '..',
57
- '..',
58
- 'node_modules',
59
- 'source-map',
60
- 'lib',
61
- 'mappings.wasm',
62
- );
63
- }),
64
- },
65
- };
82
});
83
84
afterEach(() => {
85
fetch.resetMocks();
86
});
87
72
- function expectHookNamesToEqual(map, expectedNamesArray) {
73
- // Slightly hacky since it relies on the iterable order of values()
74
- expect(Array.from(map.values())).toEqual(expectedNamesArray);
75
- }
76
-
77
- function requireText(path, encoding) {
78
- const {existsSync, readFileSync} = require('fs');
79
- if (existsSync(path)) {
80
- return Promise.resolve(readFileSync(path, encoding));
81
- } else {
82
- return Promise.reject(`File not found "${path}"`);
83
- }
84
- }
85
-
88
async function getHookNamesForComponent(Component, props = {}) {
89
const hooksTree = inspectHooks(Component, props, undefined, true);
90
const hookNames = await parseHookNames(hooksTree);
@@ -344,3 +346,68 @@ describe('parseHookNames', () => {
346
});
347
});
348
});
349
+
350
+describe('parseHookNames worker', () => {
351
+ let inspectHooks;
352
+ let parseHookNames;
353
+ let originalParseHookNamesMock;
354
+ let workerizedParseHookNamesMock;
355
+
356
+ beforeEach(() => {
357
+ window.Worker = undefined;
358
+
359
+ originalParseHookNamesMock = jest.fn();
360
+ workerizedParseHookNamesMock = jest.fn();
361
+
362
+ jest.mock('../parseHookNames/parseHookNames.js', () => {
363
+ return {
364
+ __esModule: true,
365
+ parseHookNames: originalParseHookNamesMock,
366
+ };
367
+ });
368
+
369
+ jest.mock('../parseHookNames/parseHookNames.worker.js', () => {
370
+ return {
371
+ __esModule: true,
372
+ default: () => ({
373
+ parseHookNames: workerizedParseHookNamesMock,
374
+ }),
375
+ };
376
+ });
377
+
378
+ // Mock out portion of browser API used by parseHookNames to initialize "source-map".
379
+ global.chrome = chromeGlobal;
380
+
381
+ inspectHooks = require('react-debug-tools/src/ReactDebugHooks')
382
+ .inspectHooks;
383
+ parseHookNames = require('../parseHookNames').parseHookNames;
384
+ });
385
+
386
+ async function getHookNamesForComponent(Component, props = {}) {
387
+ const hooksTree = inspectHooks(Component, props, undefined, true);
388
+ const hookNames = await parseHookNames(hooksTree);
389
+ return hookNames;
390
+ }
391
+
392
+ it('should use worker when available', async () => {
393
+ const Component = require('./__source__/__untransformed__/ComponentWithUseState')
394
+ .Component;
395
+
396
+ window.Worker = true;
397
+ // resets module so mocked worker instance can be updated
398
+ jest.resetModules();
399
+ parseHookNames = require('../parseHookNames').parseHookNames;
400
+
401
+ await getHookNamesForComponent(Component);
402
+ expect(workerizedParseHookNamesMock).toHaveBeenCalledTimes(1);
403
+ });
404
+
405
+ it('should use main thread when worker is not available', async () => {
406
+ const Component = require('./__source__/__untransformed__/ComponentWithUseState')
407
+ .Component;
408
+
409
+ await getHookNamesForComponent(Component);
410
+ expect(workerizedParseHookNamesMock).toHaveBeenCalledTimes(0);
411
+ expect(originalParseHookNamesMock).toHaveBeenCalledTimes(1);
412
+ });
413
+});
packages/react-devtools-extensions/src/parseHookNames/index.js
new
+33
@@ -0,0 +1,33 @@
1
+/* global chrome */
2
+
3
+/**
4
+ * Copyright (c) Facebook, Inc. and its affiliates.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * @flow
10
+ */
11
+
12
+// This file uses workerize to load ./parseHookNames.worker as a webworker
13
+// and instanciates it, exposing flow typed functions that can be used
14
+// on other files.
15
+
16
+import * as parseHookNamesModule from './parseHookNames';
17
+import WorkerizedParseHookNames from './parseHookNames.worker';
18
+
19
+type ParseHookNamesModule = typeof parseHookNamesModule;
20
+
21
+// $FlowFixMe
22
+const wasmMappingsURL = chrome.extension.getURL('mappings.wasm');
23
+
24
+const workerizedParseHookNames: ParseHookNamesModule = window.Worker
25
+ ? WorkerizedParseHookNames()
26
+ : parseHookNamesModule;
27
+
28
+type ParseHookNames = $PropertyType<ParseHookNamesModule, 'parseHookNames'>;
29
+
30
+export const parseHookNames: ParseHookNames = hooksTree =>
31
+ workerizedParseHookNames.parseHookNames(hooksTree, wasmMappingsURL);
32
+
33
+export const purgeCachedMetadata = workerizedParseHookNames.purgeCachedMetadata;
packages/react-devtools-extensions/src/parseHookNames/parseHookNames.js
renamed
+8
-9
@@ -1,5 +1,3 @@
1
-/* global chrome */
2
-
1
/**
2
* Copyright (c) Facebook, Inc. and its affiliates.
3
*
@@ -12,8 +10,8 @@
10
import {parse} from '@babel/parser';
11
import LRU from 'lru-cache';
12
import {SourceMapConsumer} from 'source-map';
15
-import {getHookName} from './astUtils';
16
-import {areSourceMapsAppliedToErrors} from './ErrorTester';
13
+import {getHookName} from '../astUtils';
14
+import {areSourceMapsAppliedToErrors} from '../ErrorTester';
15
import {__DEBUG__} from 'react-devtools-shared/src/constants';
16
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
17
@@ -24,7 +22,7 @@ import type {
22
} from 'react-debug-tools/src/ReactDebugHooks';
23
import type {HookNames, LRUCache} from 'react-devtools-shared/src/types';
24
import type {Thenable} from 'shared/ReactTypes';
27
-import type {SourceConsumer} from './astUtils';
25
+import type {SourceConsumer} from '../astUtils';
26
27
const SOURCE_MAP_REGEX = / ?sourceMappingURL=([^\s'"]+)/gm;
28
const MAX_SOURCE_LENGTH = 100_000_000;
@@ -103,6 +101,7 @@ const originalURLToMetadataCache: LRUCache<
101
102
export async function parseHookNames(
103
hooksTree: HooksTree,
104
+ wasmMappingsURL: string,
105
): Thenable<HookNames | null> {
106
const hooksList: Array<HooksNode> = [];
107
flattenHooksList(hooksTree, hooksList);
@@ -160,7 +159,9 @@ export async function parseHookNames(
159
}
160
161
return loadSourceFiles(locationKeyToHookSourceData)
163
- .then(() => extractAndLoadSourceMaps(locationKeyToHookSourceData))
162
+ .then(() =>
163
+ extractAndLoadSourceMaps(locationKeyToHookSourceData, wasmMappingsURL),
164
+ )
165
.then(() => parseSourceAST(locationKeyToHookSourceData))
166
.then(() => updateLruCache(locationKeyToHookSourceData))
167
.then(() => findHookNames(hooksList, locationKeyToHookSourceData));
@@ -182,6 +183,7 @@ function decodeBase64String(encoded: string): Object {
183
184
function extractAndLoadSourceMaps(
185
locationKeyToHookSourceData: Map<string, HookSourceData>,
186
+ wasmMappingsURL: string,
187
): Promise<*> {
188
// SourceMapConsumer.initialize() does nothing when running in Node (aka Jest)
189
// because the wasm file is automatically read from the file system
@@ -193,9 +195,6 @@ function extractAndLoadSourceMaps(
195
);
196
}
197
196
- // $FlowFixMe
197
- const wasmMappingsURL = chrome.extension.getURL('mappings.wasm');
198
-
198
SourceMapConsumer.initialize({'lib/mappings.wasm': wasmMappingsURL});
199
}
200
packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js
new
+4
@@ -0,0 +1,4 @@
1
+import * as parseHookNamesModule from './parseHookNames';
2
+
3
+export const parseHookNames = parseHookNamesModule.parseHookNames;
4
+export const purgeCachedMetadata = parseHookNamesModule.purgeCachedMetadata;
packages/react-devtools-extensions/webpack.config.js
+5
@@ -109,6 +109,11 @@ module.exports = {
109
},
110
],
111
},
112
+ {
113
+ test: /\.worker\.js$/,
114
+ // inline: true due to limitations with extensions
115
+ use: {loader: 'workerize-loader', options: {inline: true}},
116
+ },
117
],
118
},
119
};
packages/react-devtools-shared/src/hookNamesCache.js
+1
-1
@@ -18,7 +18,7 @@ import type {
18
} from 'react-devtools-shared/src/types';
19
import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
20
21
-const TIMEOUT = 5000;
21
+const TIMEOUT = 30000;
22
23
const Pending = 0;
24
const Resolved = 1;
yarn.lock
+7
@@ -14877,6 +14877,13 @@ worker-loader@^3.0.2:
14877
loader-utils "^2.0.0"
14878
schema-utils "^2.7.0"
14879
14880
+workerize-loader@^1.3.0:
14881
+ version "1.3.0"
14882
+ resolved "https://registry.yarnpkg.com/workerize-loader/-/workerize-loader-1.3.0.tgz#4995cf2ff2b45dd6dc60e4411e63f5ae2c704d36"
14883
+ integrity sha512-utWDc8K6embcICmRBUUkzanPgKBb8yM1OHfh6siZfiMsswE8wLCa9CWS+L7AARz0+Th4KH4ZySrqer/OJ9WuWw==
14884
+ dependencies:
14885
+ loader-utils "^2.0.0"
14886
+
14887
wrap-ansi@^2.0.0:
14888
version "2.1.0"
14889
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"