From 2a40c4b0b42fd20b683f27300ecf9c5f4626c793 Mon Sep 17 00:00:00 2001 From: lucas correia Date: Fri, 16 Jul 2021 23:52:48 -0300 Subject: [PATCH 01/10] DevTools: Parse named source AST in a worker --- .../react-devtools-extensions/package.json | 5 +-- .../src/parseHookNames.js | 31 ++++++++++--------- .../babelParser.worker.js | 5 +++ .../src/workerizedBabelParser/index.js | 31 +++++++++++++++++++ .../webpack.config.js | 5 +++ 5 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 packages/react-devtools-extensions/src/workerizedBabelParser/babelParser.worker.js create mode 100644 packages/react-devtools-extensions/src/workerizedBabelParser/index.js diff --git a/packages/react-devtools-extensions/package.json b/packages/react-devtools-extensions/package.json index da473f7613a68..4cb68ea8973a2 100644 --- a/packages/react-devtools-extensions/package.json +++ b/packages/react-devtools-extensions/package.json @@ -19,7 +19,6 @@ "update-mock-source-maps": "node ./src/__tests__/updateMockSourceMaps.js" }, "devDependencies": { - "acorn-jsx": "^5.2.0", "@babel/core": "^7.11.1", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-transform-flow-strip-types": "^7.10.4", @@ -28,6 +27,7 @@ "@babel/preset-env": "^7.11.0", "@babel/preset-flow": "^7.10.4", "@babel/preset-react": "^7.10.4", + "acorn-jsx": "^5.2.0", "archiver": "^3.0.0", "babel-core": "^7.0.0-bridge", "babel-eslint": "^9.0.0", @@ -55,7 +55,8 @@ "web-ext": "^3.0.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.10.3" + "webpack-dev-server": "^3.10.3", + "workerize-loader": "^1.3.0" }, "dependencies": { "web-ext": "^4" diff --git a/packages/react-devtools-extensions/src/parseHookNames.js b/packages/react-devtools-extensions/src/parseHookNames.js index 73745c6be3a1f..899b5a70d9ea8 100644 --- a/packages/react-devtools-extensions/src/parseHookNames.js +++ b/packages/react-devtools-extensions/src/parseHookNames.js @@ -9,11 +9,11 @@ * @flow */ -import {parse} from '@babel/parser'; import LRU from 'lru-cache'; import {SourceMapConsumer} from 'source-map'; import {getHookName} from './astUtils'; import {areSourceMapsAppliedToErrors} from './ErrorTester'; +import {workerizedParse} from './workerizedBabelParser'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache'; @@ -471,6 +471,7 @@ function loadSourceFiles( async function parseSourceAST( locationKeyToHookSourceData: Map, ): Promise<*> { + const promises = []; locationKeyToHookSourceData.forEach(hookSourceData => { if (hookSourceData.originalSourceAST !== null) { // Use cached metadata. @@ -550,24 +551,26 @@ async function parseSourceAST( const plugin = originalSourceCode.indexOf('@flow') > 0 ? 'flow' : 'typescript'; - // TODO (named hooks) Parsing should ideally be done off of the main thread. - const originalSourceAST = parse(originalSourceCode, { + const parsePromise = workerizedParse(originalSourceCode, { sourceType: 'unambiguous', plugins: ['jsx', plugin], + }).then(originalSourceAST => { + hookSourceData.originalSourceAST = originalSourceAST; + if (__DEBUG__) { + console.log( + `parseSourceAST() Caching source metadata for "${originalSourceURL}"`, + ); + } + originalURLToMetadataCache.set(originalSourceURL, { + originalSourceAST, + originalSourceCode, + }); }); - hookSourceData.originalSourceAST = originalSourceAST; - if (__DEBUG__) { - console.log( - `parseSourceAST() Caching source metadata for "${originalSourceURL}"`, - ); - } - originalURLToMetadataCache.set(originalSourceURL, { - originalSourceAST, - originalSourceCode, - }); + + promises.push(parsePromise); } }); - return Promise.resolve(); + return Promise.all(promises); } function flattenHooksList( diff --git a/packages/react-devtools-extensions/src/workerizedBabelParser/babelParser.worker.js b/packages/react-devtools-extensions/src/workerizedBabelParser/babelParser.worker.js new file mode 100644 index 0000000000000..dc94234daab98 --- /dev/null +++ b/packages/react-devtools-extensions/src/workerizedBabelParser/babelParser.worker.js @@ -0,0 +1,5 @@ +import {parse} from '@babel/parser'; + +export function workerizedParse(...params) { + return parse(...params); +} diff --git a/packages/react-devtools-extensions/src/workerizedBabelParser/index.js b/packages/react-devtools-extensions/src/workerizedBabelParser/index.js new file mode 100644 index 0000000000000..04d0d07e46959 --- /dev/null +++ b/packages/react-devtools-extensions/src/workerizedBabelParser/index.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// This file uses workerize to load ./babelParse.worker as a webworker +// and instanciates it, exposing flow typed functions that can be used +// on other files. + +import {parse} from '@babel/parser'; +import WorkerizedBabelParser from './babelParser.worker'; + +import type {ParserOptions} from '@babel/parser'; + +const workerizedBabelParser = Worker && WorkerizedBabelParser(); + +export const workerizedParse = async ( + input: string, + options?: ParserOptions, +) => { + // Checks if worker is not available runs regular babel parse + if (workerizedBabelParser) { + const workerParse: typeof parse = WorkerizedBabelParser().workerizedParse; + return workerParse(input, options); + } + return parse(input, options); +}; diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index 94a12ebdd1118..3b89bf7355eb3 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -109,6 +109,11 @@ module.exports = { }, ], }, + { + test: /\.worker\.js$/, + // inline: true due to limitations with extensions + use: {loader: 'workerize-loader', options: {inline: true}}, + }, ], }, }; From 11fced84e5fa4464d7cce99e7f58a76ccf8e8b07 Mon Sep 17 00:00:00 2001 From: lucas correia Date: Sat, 17 Jul 2021 00:48:59 -0300 Subject: [PATCH 02/10] DevTools: Fix flow types declarations for workerizedParse --- .../src/workerizedBabelParser/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools-extensions/src/workerizedBabelParser/index.js b/packages/react-devtools-extensions/src/workerizedBabelParser/index.js index 04d0d07e46959..288861f77034e 100644 --- a/packages/react-devtools-extensions/src/workerizedBabelParser/index.js +++ b/packages/react-devtools-extensions/src/workerizedBabelParser/index.js @@ -14,13 +14,13 @@ import {parse} from '@babel/parser'; import WorkerizedBabelParser from './babelParser.worker'; -import type {ParserOptions} from '@babel/parser'; - const workerizedBabelParser = Worker && WorkerizedBabelParser(); export const workerizedParse = async ( input: string, - options?: ParserOptions, + options?: { + plugins: string[], + }, ) => { // Checks if worker is not available runs regular babel parse if (workerizedBabelParser) { From be66d479635fae3d0e972e67afe65fe9942b63ba Mon Sep 17 00:00:00 2001 From: lucas correia Date: Sat, 17 Jul 2021 02:27:03 -0300 Subject: [PATCH 03/10] DevTools: mock worker for tests --- .../src/__tests__/parseHookNames-test.js | 22 +++++++++++++++++++ .../src/workerizedBabelParser/index.js | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js b/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js index 0b10ffee2bee4..c831bde249656 100644 --- a/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js +++ b/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js @@ -11,6 +11,8 @@ // This is done to control if and how the code is transformed at runtime. // Do not declare test components within this test file as it is very fragile. +const babelParserWorker = require('../workerizedBabelParser/babelParser.worker.js'); + describe('parseHookNames', () => { let fetchMock; let inspectHooks; @@ -23,6 +25,26 @@ describe('parseHookNames', () => { console.trace('source-map-support'); }); + class Worker { + constructor(stringUrl) { + this.url = stringUrl; + this.onmessage = () => {}; + } + + postMessage(msg) { + this.onmessage(msg); + } + } + + window.Worker = Worker; + + jest.mock('../workerizedBabelParser/babelParser.worker.js', () => { + return { + __esModule: true, + default: () => babelParserWorker, + }; + }); + fetchMock = require('jest-fetch-mock'); fetchMock.enableMocks(); diff --git a/packages/react-devtools-extensions/src/workerizedBabelParser/index.js b/packages/react-devtools-extensions/src/workerizedBabelParser/index.js index 288861f77034e..ab0f6ae34be46 100644 --- a/packages/react-devtools-extensions/src/workerizedBabelParser/index.js +++ b/packages/react-devtools-extensions/src/workerizedBabelParser/index.js @@ -14,7 +14,7 @@ import {parse} from '@babel/parser'; import WorkerizedBabelParser from './babelParser.worker'; -const workerizedBabelParser = Worker && WorkerizedBabelParser(); +const workerizedBabelParser = window.Worker && WorkerizedBabelParser(); export const workerizedParse = async ( input: string, From 7346b501a3b763865b82c2bba54ca5fc4df02ac6 Mon Sep 17 00:00:00 2001 From: lucas correia Date: Sat, 17 Jul 2021 02:30:54 -0300 Subject: [PATCH 04/10] DevTools: use worker instance instead of creating a new one --- .../src/workerizedBabelParser/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-extensions/src/workerizedBabelParser/index.js b/packages/react-devtools-extensions/src/workerizedBabelParser/index.js index ab0f6ae34be46..246742ca188c5 100644 --- a/packages/react-devtools-extensions/src/workerizedBabelParser/index.js +++ b/packages/react-devtools-extensions/src/workerizedBabelParser/index.js @@ -16,6 +16,8 @@ import WorkerizedBabelParser from './babelParser.worker'; const workerizedBabelParser = window.Worker && WorkerizedBabelParser(); +type Parse = typeof parse; + export const workerizedParse = async ( input: string, options?: { @@ -24,7 +26,7 @@ export const workerizedParse = async ( ) => { // Checks if worker is not available runs regular babel parse if (workerizedBabelParser) { - const workerParse: typeof parse = WorkerizedBabelParser().workerizedParse; + const workerParse: Parse = workerizedBabelParser.workerizedParse; return workerParse(input, options); } return parse(input, options); From 1bebee80d49b6910ef47af30282b724ccdd2c212 Mon Sep 17 00:00:00 2001 From: lucas correia Date: Sat, 17 Jul 2021 04:36:30 -0300 Subject: [PATCH 05/10] DevTools: isolate worker tests and check if function calls are addressed correctly --- .../src/__tests__/parseHookNames-test.js | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js b/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js index c831bde249656..56cb1f62e4c58 100644 --- a/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js +++ b/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js @@ -11,12 +11,15 @@ // This is done to control if and how the code is transformed at runtime. // Do not declare test components within this test file as it is very fragile. +const {parse} = require('@babel/parser'); const babelParserWorker = require('../workerizedBabelParser/babelParser.worker.js'); describe('parseHookNames', () => { let fetchMock; let inspectHooks; let parseHookNames; + let babelParserMock; + let workerizedParseMock; beforeEach(() => { jest.resetModules(); @@ -25,23 +28,22 @@ describe('parseHookNames', () => { console.trace('source-map-support'); }); - class Worker { - constructor(stringUrl) { - this.url = stringUrl; - this.onmessage = () => {}; - } + window.Worker = undefined; - postMessage(msg) { - this.onmessage(msg); - } - } + babelParserMock = jest.fn(parse); + workerizedParseMock = jest.fn(babelParserWorker.workerizedParse); - window.Worker = Worker; + jest.mock('@babel/parser', () => { + return { + __esModule: true, + parse: babelParserMock, + }; + }); jest.mock('../workerizedBabelParser/babelParser.worker.js', () => { return { __esModule: true, - default: () => babelParserWorker, + default: () => ({workerizedParse: workerizedParseMock}), }; }); @@ -111,6 +113,30 @@ describe('parseHookNames', () => { return hookNames; } + it('should use worker when available', async () => { + const Component = require('./__source__/__untransformed__/ComponentWithUseState') + .Component; + + window.Worker = true; + // resets module so mocked worker instance can be updated + jest.resetModules(); + parseHookNames = require('../parseHookNames').parseHookNames; + + const hookNames = await getHookNamesForComponent(Component); + expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz']); + expect(workerizedParseMock).toHaveBeenCalledTimes(3); + }); + + it('should use babel parser when worker is not available', async () => { + const Component = require('./__source__/__untransformed__/ComponentWithUseState') + .Component; + + const hookNames = await getHookNamesForComponent(Component); + expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz']); + expect(workerizedParseMock).toHaveBeenCalledTimes(0); + expect(babelParserMock).toHaveBeenCalledTimes(3); + }); + it('should parse names for useState()', async () => { const Component = require('./__source__/__untransformed__/ComponentWithUseState') .Component; From 485567ae7d7bb29cfb636b56ca75eb5a947f705f Mon Sep 17 00:00:00 2001 From: lucas correia Date: Mon, 19 Jul 2021 22:34:02 -0300 Subject: [PATCH 06/10] DevTools: move parseHookNames function to worker --- .../src/parseHookNames/index.js | 33 +++++++++++++ .../{ => parseHookNames}/parseHookNames.js | 48 +++++++++---------- .../parseHookNames/parseHookNames.worker.js | 4 ++ .../babelParser.worker.js | 5 -- .../src/workerizedBabelParser/index.js | 33 ------------- 5 files changed, 59 insertions(+), 64 deletions(-) create mode 100644 packages/react-devtools-extensions/src/parseHookNames/index.js rename packages/react-devtools-extensions/src/{ => parseHookNames}/parseHookNames.js (95%) create mode 100644 packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js delete mode 100644 packages/react-devtools-extensions/src/workerizedBabelParser/babelParser.worker.js delete mode 100644 packages/react-devtools-extensions/src/workerizedBabelParser/index.js diff --git a/packages/react-devtools-extensions/src/parseHookNames/index.js b/packages/react-devtools-extensions/src/parseHookNames/index.js new file mode 100644 index 0000000000000..da864aa27085e --- /dev/null +++ b/packages/react-devtools-extensions/src/parseHookNames/index.js @@ -0,0 +1,33 @@ +/* global chrome */ + +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// This file uses workerize to load ./parseHookNames.worker as a webworker +// and instanciates it, exposing flow typed functions that can be used +// on other files. + +import * as parseHookNamesModule from './parseHookNames'; +import WorkerizedParseHookNames from './parseHookNames.worker'; + +type ParseHookNamesModule = typeof parseHookNamesModule; + +// $FlowFixMe +const wasmMappingsURL = chrome.extension.getURL('mappings.wasm'); + +const workerizedParseHookNames: ParseHookNamesModule = window.Worker + ? WorkerizedParseHookNames() + : parseHookNamesModule; + +type ParseHookNames = $PropertyType; + +export const parseHookNames: ParseHookNames = hooksTree => + workerizedParseHookNames.parseHookNames(hooksTree, wasmMappingsURL); + +export const purgeCachedMetadata = workerizedParseHookNames.purgeCachedMetadata; diff --git a/packages/react-devtools-extensions/src/parseHookNames.js b/packages/react-devtools-extensions/src/parseHookNames/parseHookNames.js similarity index 95% rename from packages/react-devtools-extensions/src/parseHookNames.js rename to packages/react-devtools-extensions/src/parseHookNames/parseHookNames.js index 899b5a70d9ea8..f68c37135b8b7 100644 --- a/packages/react-devtools-extensions/src/parseHookNames.js +++ b/packages/react-devtools-extensions/src/parseHookNames/parseHookNames.js @@ -1,5 +1,3 @@ -/* global chrome */ - /** * Copyright (c) Facebook, Inc. and its affiliates. * @@ -9,11 +7,11 @@ * @flow */ +import {parse} from '@babel/parser'; import LRU from 'lru-cache'; import {SourceMapConsumer} from 'source-map'; -import {getHookName} from './astUtils'; -import {areSourceMapsAppliedToErrors} from './ErrorTester'; -import {workerizedParse} from './workerizedBabelParser'; +import {getHookName} from '../astUtils'; +import {areSourceMapsAppliedToErrors} from '../ErrorTester'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache'; @@ -24,7 +22,7 @@ import type { } from 'react-debug-tools/src/ReactDebugHooks'; import type {HookNames, LRUCache} from 'react-devtools-shared/src/types'; import type {Thenable} from 'shared/ReactTypes'; -import type {SourceConsumer} from './astUtils'; +import type {SourceConsumer} from '../astUtils'; const SOURCE_MAP_REGEX = / ?sourceMappingURL=([^\s'"]+)/gm; const MAX_SOURCE_LENGTH = 100_000_000; @@ -103,6 +101,7 @@ const originalURLToMetadataCache: LRUCache< export async function parseHookNames( hooksTree: HooksTree, + wasmMappingsURL: string, ): Thenable { const hooksList: Array = []; flattenHooksList(hooksTree, hooksList); @@ -160,7 +159,9 @@ export async function parseHookNames( } return loadSourceFiles(locationKeyToHookSourceData) - .then(() => extractAndLoadSourceMaps(locationKeyToHookSourceData)) + .then(() => + extractAndLoadSourceMaps(locationKeyToHookSourceData, wasmMappingsURL), + ) .then(() => parseSourceAST(locationKeyToHookSourceData)) .then(() => updateLruCache(locationKeyToHookSourceData)) .then(() => findHookNames(hooksList, locationKeyToHookSourceData)); @@ -182,6 +183,7 @@ function decodeBase64String(encoded: string): Object { function extractAndLoadSourceMaps( locationKeyToHookSourceData: Map, + wasmMappingsURL: string, ): Promise<*> { // SourceMapConsumer.initialize() does nothing when running in Node (aka Jest) // because the wasm file is automatically read from the file system @@ -193,9 +195,6 @@ function extractAndLoadSourceMaps( ); } - // $FlowFixMe - const wasmMappingsURL = chrome.extension.getURL('mappings.wasm'); - SourceMapConsumer.initialize({'lib/mappings.wasm': wasmMappingsURL}); } @@ -471,7 +470,6 @@ function loadSourceFiles( async function parseSourceAST( locationKeyToHookSourceData: Map, ): Promise<*> { - const promises = []; locationKeyToHookSourceData.forEach(hookSourceData => { if (hookSourceData.originalSourceAST !== null) { // Use cached metadata. @@ -551,26 +549,24 @@ async function parseSourceAST( const plugin = originalSourceCode.indexOf('@flow') > 0 ? 'flow' : 'typescript'; - const parsePromise = workerizedParse(originalSourceCode, { + // TODO (named hooks) Parsing should ideally be done off of the main thread. + const originalSourceAST = parse(originalSourceCode, { sourceType: 'unambiguous', plugins: ['jsx', plugin], - }).then(originalSourceAST => { - hookSourceData.originalSourceAST = originalSourceAST; - if (__DEBUG__) { - console.log( - `parseSourceAST() Caching source metadata for "${originalSourceURL}"`, - ); - } - originalURLToMetadataCache.set(originalSourceURL, { - originalSourceAST, - originalSourceCode, - }); }); - - promises.push(parsePromise); + hookSourceData.originalSourceAST = originalSourceAST; + if (__DEBUG__) { + console.log( + `parseSourceAST() Caching source metadata for "${originalSourceURL}"`, + ); + } + originalURLToMetadataCache.set(originalSourceURL, { + originalSourceAST, + originalSourceCode, + }); } }); - return Promise.all(promises); + return Promise.resolve(); } function flattenHooksList( diff --git a/packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js b/packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js new file mode 100644 index 0000000000000..7843527e4d963 --- /dev/null +++ b/packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js @@ -0,0 +1,4 @@ +import * as parseHookNamesModule from './parseHookNames'; + +export const parseHookNames = parseHookNamesModule.parseHookNames; +export const purgeCachedMetadata = parseHookNamesModule.purgeCachedMetadata; diff --git a/packages/react-devtools-extensions/src/workerizedBabelParser/babelParser.worker.js b/packages/react-devtools-extensions/src/workerizedBabelParser/babelParser.worker.js deleted file mode 100644 index dc94234daab98..0000000000000 --- a/packages/react-devtools-extensions/src/workerizedBabelParser/babelParser.worker.js +++ /dev/null @@ -1,5 +0,0 @@ -import {parse} from '@babel/parser'; - -export function workerizedParse(...params) { - return parse(...params); -} diff --git a/packages/react-devtools-extensions/src/workerizedBabelParser/index.js b/packages/react-devtools-extensions/src/workerizedBabelParser/index.js deleted file mode 100644 index 246742ca188c5..0000000000000 --- a/packages/react-devtools-extensions/src/workerizedBabelParser/index.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -// This file uses workerize to load ./babelParse.worker as a webworker -// and instanciates it, exposing flow typed functions that can be used -// on other files. - -import {parse} from '@babel/parser'; -import WorkerizedBabelParser from './babelParser.worker'; - -const workerizedBabelParser = window.Worker && WorkerizedBabelParser(); - -type Parse = typeof parse; - -export const workerizedParse = async ( - input: string, - options?: { - plugins: string[], - }, -) => { - // Checks if worker is not available runs regular babel parse - if (workerizedBabelParser) { - const workerParse: Parse = workerizedBabelParser.workerizedParse; - return workerParse(input, options); - } - return parse(input, options); -}; From c1190852606bd535c01e51ecdd592adf9972d6b2 Mon Sep 17 00:00:00 2001 From: lucas correia Date: Mon, 19 Jul 2021 22:34:16 -0300 Subject: [PATCH 07/10] DevTools: adapt tests to parseHookNames changes --- .../src/__tests__/parseHookNames-test.js | 177 ++++++++++-------- 1 file changed, 98 insertions(+), 79 deletions(-) diff --git a/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js b/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js index 56cb1f62e4c58..67e57871e4a13 100644 --- a/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js +++ b/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js @@ -11,15 +11,41 @@ // This is done to control if and how the code is transformed at runtime. // Do not declare test components within this test file as it is very fragile. -const {parse} = require('@babel/parser'); -const babelParserWorker = require('../workerizedBabelParser/babelParser.worker.js'); +function expectHookNamesToEqual(map, expectedNamesArray) { + // Slightly hacky since it relies on the iterable order of values() + expect(Array.from(map.values())).toEqual(expectedNamesArray); +} + +function requireText(path, encoding) { + const {existsSync, readFileSync} = require('fs'); + if (existsSync(path)) { + return Promise.resolve(readFileSync(path, encoding)); + } else { + return Promise.reject(`File not found "${path}"`); + } +} + +const chromeGlobal = { + extension: { + getURL: jest.fn((...args) => { + const {join} = require('path'); + return join( + __dirname, + '..', + '..', + 'node_modules', + 'source-map', + 'lib', + 'mappings.wasm', + ); + }), + }, +}; describe('parseHookNames', () => { let fetchMock; let inspectHooks; let parseHookNames; - let babelParserMock; - let workerizedParseMock; beforeEach(() => { jest.resetModules(); @@ -28,28 +54,12 @@ describe('parseHookNames', () => { console.trace('source-map-support'); }); - window.Worker = undefined; - - babelParserMock = jest.fn(parse); - workerizedParseMock = jest.fn(babelParserWorker.workerizedParse); - - jest.mock('@babel/parser', () => { - return { - __esModule: true, - parse: babelParserMock, - }; - }); - - jest.mock('../workerizedBabelParser/babelParser.worker.js', () => { - return { - __esModule: true, - default: () => ({workerizedParse: workerizedParseMock}), - }; - }); - fetchMock = require('jest-fetch-mock'); fetchMock.enableMocks(); + // Mock out portion of browser API used by parseHookNames to initialize "source-map". + global.chrome = chromeGlobal; + inspectHooks = require('react-debug-tools/src/ReactDebugHooks') .inspectHooks; parseHookNames = require('../parseHookNames').parseHookNames; @@ -69,74 +79,18 @@ describe('parseHookNames', () => { fetchMock.mockIf(/.+$/, request => { return requireText(request.url, 'utf8'); }); - - // Mock out portion of browser API used by parseHookNames to initialize "source-map". - global.chrome = { - extension: { - getURL: jest.fn((...args) => { - const {join} = require('path'); - return join( - __dirname, - '..', - '..', - 'node_modules', - 'source-map', - 'lib', - 'mappings.wasm', - ); - }), - }, - }; }); afterEach(() => { fetch.resetMocks(); }); - function expectHookNamesToEqual(map, expectedNamesArray) { - // Slightly hacky since it relies on the iterable order of values() - expect(Array.from(map.values())).toEqual(expectedNamesArray); - } - - function requireText(path, encoding) { - const {existsSync, readFileSync} = require('fs'); - if (existsSync(path)) { - return Promise.resolve(readFileSync(path, encoding)); - } else { - return Promise.reject(`File not found "${path}"`); - } - } - async function getHookNamesForComponent(Component, props = {}) { const hooksTree = inspectHooks(Component, props, undefined, true); const hookNames = await parseHookNames(hooksTree); return hookNames; } - it('should use worker when available', async () => { - const Component = require('./__source__/__untransformed__/ComponentWithUseState') - .Component; - - window.Worker = true; - // resets module so mocked worker instance can be updated - jest.resetModules(); - parseHookNames = require('../parseHookNames').parseHookNames; - - const hookNames = await getHookNamesForComponent(Component); - expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz']); - expect(workerizedParseMock).toHaveBeenCalledTimes(3); - }); - - it('should use babel parser when worker is not available', async () => { - const Component = require('./__source__/__untransformed__/ComponentWithUseState') - .Component; - - const hookNames = await getHookNamesForComponent(Component); - expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz']); - expect(workerizedParseMock).toHaveBeenCalledTimes(0); - expect(babelParserMock).toHaveBeenCalledTimes(3); - }); - it('should parse names for useState()', async () => { const Component = require('./__source__/__untransformed__/ComponentWithUseState') .Component; @@ -392,3 +346,68 @@ describe('parseHookNames', () => { }); }); }); + +describe('parseHookNames worker', () => { + let inspectHooks; + let parseHookNames; + let originalParseHookNamesMock; + let workerizedParseHookNamesMock; + + beforeEach(() => { + window.Worker = undefined; + + originalParseHookNamesMock = jest.fn(); + workerizedParseHookNamesMock = jest.fn(); + + jest.mock('../parseHookNames/parseHookNames.js', () => { + return { + __esModule: true, + parseHookNames: originalParseHookNamesMock, + }; + }); + + jest.mock('../parseHookNames/parseHookNames.worker.js', () => { + return { + __esModule: true, + default: () => ({ + parseHookNames: workerizedParseHookNamesMock, + }), + }; + }); + + // Mock out portion of browser API used by parseHookNames to initialize "source-map". + global.chrome = chromeGlobal; + + inspectHooks = require('react-debug-tools/src/ReactDebugHooks') + .inspectHooks; + parseHookNames = require('../parseHookNames').parseHookNames; + }); + + async function getHookNamesForComponent(Component, props = {}) { + const hooksTree = inspectHooks(Component, props, undefined, true); + const hookNames = await parseHookNames(hooksTree); + return hookNames; + } + + it('should use worker when available', async () => { + const Component = require('./__source__/__untransformed__/ComponentWithUseState') + .Component; + + window.Worker = true; + // resets module so mocked worker instance can be updated + jest.resetModules(); + parseHookNames = require('../parseHookNames').parseHookNames; + + await getHookNamesForComponent(Component); + expect(workerizedParseHookNamesMock).toHaveBeenCalledTimes(1); + }); + + it('should use main thread when worker is not available', async () => { + const Component = require('./__source__/__untransformed__/ComponentWithUseState') + .Component; + + await getHookNamesForComponent(Component); + expect(workerizedParseHookNamesMock).toHaveBeenCalledTimes(0); + expect(originalParseHookNamesMock).toHaveBeenCalledTimes(1); + }); +}); From 754f8a3c1f5076eb56cd3becc4f1f55ae1383d48 Mon Sep 17 00:00:00 2001 From: lucas correia Date: Mon, 19 Jul 2021 22:34:30 -0300 Subject: [PATCH 08/10] DevTools: update yarn.lock --- yarn.lock | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/yarn.lock b/yarn.lock index c0bddfe82c64e..03daa69ff8127 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14877,6 +14877,13 @@ worker-loader@^3.0.2: loader-utils "^2.0.0" schema-utils "^2.7.0" +workerize-loader@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/workerize-loader/-/workerize-loader-1.3.0.tgz#4995cf2ff2b45dd6dc60e4411e63f5ae2c704d36" + integrity sha512-utWDc8K6embcICmRBUUkzanPgKBb8yM1OHfh6siZfiMsswE8wLCa9CWS+L7AARz0+Th4KH4ZySrqer/OJ9WuWw== + dependencies: + loader-utils "^2.0.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" From ea30ba75a94bb0142335c884657aca43e9ac85f7 Mon Sep 17 00:00:00 2001 From: lucas correia Date: Tue, 20 Jul 2021 11:33:10 -0300 Subject: [PATCH 09/10] DevTools: increase hookNamesCache timeout --- packages/react-devtools-shared/src/hookNamesCache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/hookNamesCache.js b/packages/react-devtools-shared/src/hookNamesCache.js index f385c72428525..070dafeb1d8eb 100644 --- a/packages/react-devtools-shared/src/hookNamesCache.js +++ b/packages/react-devtools-shared/src/hookNamesCache.js @@ -18,7 +18,7 @@ import type { } from 'react-devtools-shared/src/types'; import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks'; -const TIMEOUT = 5000; +const TIMEOUT = 30000; const Pending = 0; const Resolved = 1; From d2b962de0de1878b5ff6aed6e2d64e846edac7c0 Mon Sep 17 00:00:00 2001 From: lucas correia Date: Tue, 20 Jul 2021 15:24:23 -0300 Subject: [PATCH 10/10] DevTools: allow blob: on content_security_policy for firefox --- packages/react-devtools-extensions/firefox/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index a97422cedf86d..44327c8566f74 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -32,7 +32,7 @@ "devtools_page": "main.html", - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "content_security_policy": "script-src 'self' 'unsafe-eval' blob:; object-src 'self'", "web_accessible_resources": [ "main.html", "panel.html",