* Revert "Only show DevTools warning about unrecognized build in Chrome (#22571)"
This reverts commit b72dc8e930.
* Revert "Show warning in UI when duplicate installations of DevTools extension are detected (#22563)"
This reverts commit 930c9e7eeb.
* Revert "Prevent errors/crashing when multiple installs of DevTools are present (#22517)"
This reverts commit 545d4c2de7.
* Remove all references to passing extensionId in postMessage
* Keep build changes
* lint
This commit adds code to all React bundles to explicitly register the beginning and ending of the module. This is done by creating Error objects (which capture the file name, line number, and column number) and passing them explicitly to a DevTools hook (when present).
Next, as the Scheduling Profiler logs metadata to the User Timing API, it prints these module ranges along with other metadata (like Lane values and profiler version number).
Lastly, the Scheduling Profiler UI compares stack frames to these ranges when drawing the flame graph and dims or de-emphasizes frames that fall within an internal module.
The net effect of this is that user code (and 3rd party code) stands out clearly in the flame graph while React internal modules are dimmed.
Internal module ranges are completely optional. Older profiling samples, or ones recorded without the React DevTools extension installed, will simply not dim the internal frames.
## Summary
This commit is a proposal for handling duplicate installation of DevTools, in particular scoped to duplicates such as a dev build or the internal versions of DevTools installed alongside the Chrome Web Store extension.
Specifically, this commit makes it so when another instance of the DevTools extension is installed alongside the extension installed from the Chrome Web Store, we don't produce a stream of errors or crash Chrome, which is what would usually happen in this case.
### Detecting Duplicate Installations
- First, we check what type of installation the extension is: from the Chrome Web Store, the internal build of the extension, or a local development build.
- If the extension is from the **Chrome Web Store**:
- During initialization, we first check if the internal or local builds of the extension have already been installed and are enabled. To do this, we send a [cross-extension message](https://developer.chrome.com/docs/extensions/mv3/messaging/#external) to the internal and local builds of the extension using their extension IDs.
- We can do this because the extension ID for the internal build (and for the Chrome Web Store) is a stable ID.
- For the local build, at build time we hardcode a [`key` in the `manifest.json`](https://developer.chrome.com/docs/extensions/mv2/manifest/key/) which allows us to have a stable ID even for local builds.
- If we detect that the internal or local extensions are already installed, then we skip initializing the current extension altogether so as to not conflict with the other versions. This means we don't initialize the frontend or the backend at all.
- If the extension is the **Internal Build**:
- During initialization, we first check if the local builds of the extension has already been installed and is enabled. To do this, we send a [cross-extension message](https://developer.chrome.com/docs/extensions/mv3/messaging/#external) to the local build of the extension using its extension ID.
- We can do this for the local build because at build time we hardcode a [`key` in the `manifest.json`](https://developer.chrome.com/docs/extensions/mv2/manifest/key/) which allows us to have a stable ID even for local builds.
- If we detect that the local extension is already installed, then we skip initializing the current extension altogether so as to not conflict with the that version. This means we don't initialize the frontend or the backend at all.
- If the extension is a **Local Dev Build**:
- Since other extensions check for the existence of this extension and disable themselves if they detect it, we don't need any special handling during initialization and assume that there are no duplicate extensions. This means that we will generally prefer keeping this extension enabled.
This behavior means that the order of priority for keeping an extension enabled is the following:
1. Local build
2. Internal build
3. Public build
### Preventing duplicate backend initialization
Note that the backend is injected and initialized by the content script listening to a message posted to the inspected window (via `postMessage`). Since the content script will be injected twice, once each by each instance of the extension, even if we initialize the extension once, both content scripts would still receive the single message posted from the single frontend, and it would then still inject and initialize the backend twice.
In order to prevent this, we also add the extension ID to the message for injecting the backend. That way each content script can check if the message comes from its own extension, and if not it can ignore the message and avoid double injecting the backend.
### Other approaches
- I considered using the [`chrome.management`](https://developer.chrome.com/docs/extensions/reference/management/) API generally to detect other installations, but that requires adding additional permissions to our production extension, which didn't seem ideal.
- I also considered a few options of writing a special flag to the inspected window and checking for it before initializing the extension. However, it's hard to avoid race conditions in that case, and it seemed more reliable to check specifically for the WebStore extension, which is realistically where we would encounter the overlap.
### Rollout
- This commit needs to be published and rolled out to the Chrome Web Store first.
- After this commit is published to the Chrome Web Store, any duplicate instances of the extension that are built and installed after this commit will no longer conflict with the Chrome Web Store version.
### Next Steps
- In a subsequent PR, I will extend this code to show a warning when duplicate extensions have been detected.
Part of #22486
## How did you test this change?
### Basic Testing
- yarn flow
- yarn test
- yarn test-build-devtools
### Double installation testing
Testing double-installed extensions for this commit is tricky because we are relying on the extension ID of the internal and Chrome Web Store extensions, but we obviously can't actually test the Web Store version (since we can't modify the already published version).
In order to simulate duplicate extensions installed, I did the following process:
- Built separate extensions where I hardcoded a constant for whether the extension is internal or public (e.g. `EXTENSION_INSTALLATION_TYPE = 'internal'`). Then I installed these built extensions corresponding to the "internal" and "Web Store" builds.
- Build and run the regular development extension (with `yarn build:chrome:dev && yarn test:chrome`), using the extension IDs of the previously built extensions as the "internal" and "public" extension IDs.
With this set up in place, I tested the following on pages both with and without React:
- When only the local extension enabled, DevTools works normally.
- When only the "internal" extension enabled, DevTools works normally.
- When only the "public" extension enabled, DevTools works normally.
- When "internal" and "public" extensions are installed, "public" extension is disabled and "internal" extension works normally.
- When the local extension runs alongside the other extensions, other extensions disable themselves and local build works normally.
- When we can't recognize what type of build the extension corresponds to, we show an error.
- When all 3 extensions are installed and enabled in all different combinations, DevTools no longer produces errors or crashes Chrome, and works normally.
* Revise ESLint rules for string coercion
Currently, react uses `'' + value` to coerce mixed values to strings.
This code will throw for Temporal objects or symbols.
To make string-coercion safer and to improve user-facing error messages,
This commit adds a new ESLint rule called `safe-string-coercion`.
This rule has two modes: a production mode and a non-production mode.
* If the `isProductionUserAppCode` option is true, then `'' + value`
coercions are allowed (because they're faster, although they may
throw) and `String(value)` coercions are disallowed. Exception:
when building error messages or running DEV-only code in prod
files, `String()` should be used because it won't throw.
* If the `isProductionUserAppCode` option is false, then `'' + value`
coercions are disallowed (because they may throw, and in non-prod
code it's not worth the risk) and `String(value)` are allowed.
Production mode is used for all files which will be bundled with
developers' userland apps. Non-prod mode is used for all other React
code: tests, DEV blocks, devtools extension, etc.
In production mode, in addiiton to flagging `String(value)` calls,
the rule will also flag `'' + value` or `value + ''` coercions that may
throw. The rule is smart enough to silence itself in the following
"will never throw" cases:
* When the coercion is wrapped in a `typeof` test that restricts to safe
(non-symbol, non-object) types. Example:
if (typeof value === 'string' || typeof value === 'number') {
thisWontReport('' + value);
}
* When what's being coerced is a unary function result, because unary
functions never return an object or a symbol.
* When the coerced value is a commonly-used numeric identifier:
`i`, `idx`, or `lineNumber`.
* When the statement immeidately before the coercion is a DEV-only
call to a function from shared/CheckStringCoercion.js. This call is a
no-op in production, but in DEV it will show a console error
explaining the problem, then will throw right after a long explanatory
code comment so that debugger users will have an idea what's going on.
The check function call must be in the following format:
if (__DEV__) {
checkXxxxxStringCoercion(value);
};
Manually disabling the rule is usually not necessary because almost all
prod use of the `'' + value` pattern falls into one of the categories
above. But in the rare cases where the rule isn't smart enough to detect
safe usage (e.g. when a coercion is inside a nested ternary operator),
manually disabling the rule will be needed.
The rule should also be manually disabled in prod error handling code
where `String(value)` should be used for coercions, because it'd be
bad to throw while building an error message or stack trace!
The prod and non-prod modes have differentiated error messages to
explain how to do a proper coercion in that mode.
If a production check call is needed but is missing or incorrect
(e.g. not in a DEV block or not immediately before the coercion), then
a context-sensitive error message will be reported so that developers
can figure out what's wrong and how to fix the problem.
Because string coercions are now handled by the `safe-string-coercion`
rule, the `no-primitive-constructor` rule no longer flags `String()`
usage. It still flags `new String(value)` because that usage is almost
always a bug.
* Add DEV-only string coercion check functions
This commit adds DEV-only functions to check whether coercing
values to strings using the `'' + value` pattern will throw. If it will
throw, these functions will:
1. Display a console error with a friendly error message describing
the problem and the developer can fix it.
2. Perform the coercion, which will throw. Right before the line where
the throwing happens, there's a long code comment that will help
debugger users (or others looking at the exception call stack) figure
out what happened and how to fix the problem.
One of these check functions should be called before all string coercion
of user-provided values, except when the the coercion is guaranteed not
to throw, e.g.
* if inside a typeof check like `if (typeof value === 'string')`
* if coercing the result of a unary function like `+value` or `value++`
* if coercing a variable named in a whitelist of numeric identifiers:
`i`, `idx`, or `lineNumber`.
The new `safe-string-coercion` internal ESLint rule enforces that
these check functions are called when they are required.
Only use these check functions in production code that will be bundled
with user apps. For non-prod code (and for production error-handling
code), use `String(value)` instead which may be a little slower but will
never throw.
* Add failing tests for string coercion
Added failing tests to verify:
* That input, select, and textarea elements with value and defaultValue
set to Temporal-like objects which will throw when coerced to string
using the `'' + value` pattern.
* That text elements will throw for Temporal-like objects
* That dangerouslySetInnerHTML will *not* throw for Temporal-like
objects because this value is not cast to a string before passing to
the DOM.
* That keys that are Temporal-like objects will throw
All tests above validate the friendly error messages thrown.
* Use `String(value)` for coercion in non-prod files
This commit switches non-production code from `'' + value` (which
throws for Temporal objects and symbols) to instead use `String(value)`
which won't throw for these or other future plus-phobic types.
"Non-produciton code" includes anything not bundled into user apps:
* Tests and test utilities. Note that I didn't change legacy React
test fixtures because I assumed it was good for those files to
act just like old React, including coercion behavior.
* Build scripts
* Dev tools package - In addition to switching to `String`, I also
removed special-case code for coercing symbols which is now
unnecessary.
* Add DEV-only string coercion checks to prod files
This commit adds DEV-only function calls to to check if string coercion
using `'' + value` will throw, which it will if the value is a Temporal
object or a symbol because those types can't be added with `+`.
If it will throw, then in DEV these checks will show a console error
to help the user undertsand what went wrong and how to fix the
problem. After emitting the console error, the check functions will
retry the coercion which will throw with a call stack that's easy (or
at least easier!) to troubleshoot because the exception happens right
after a long comment explaining the issue. So whether the user is in
a debugger, looking at the browser console, or viewing the in-browser
DEV call stack, it should be easy to understand and fix the problem.
In most cases, the safe-string-coercion ESLint rule is smart enough to
detect when a coercion is safe. But in rare cases (e.g. when a coercion
is inside a ternary) this rule will have to be manually disabled.
This commit also switches error-handling code to use `String(value)`
for coercion, because it's bad to crash when you're trying to build
an error message or a call stack! Because `String()` is usually
disallowed by the `safe-string-coercion` ESLint rule in production
code, the rule must be disabled when `String()` is used.
Update all our local scripts to use `build` instead of `build2`.
There are still downstream scripts that depend on `build2`, though, so
we can't remove it yet.
Replaced network.onRequestFinished() caching with network.getHAR() so that we can avoid redundantly (pre) caching JavaScript content. In the event that the HAR log doesn't contain a match, we'll fall back to fetching from the Network (and hoping for a cache hit from that layer).
I've tested both internally (internal Facebook DEV server) and externally (Code Sandbox) and it seems like this approach results in cache hits, so long as DevTools is opened when the page loads. (Otherwise it falls back to fetch().)
This commit builds on PR #22260 and makes the following changes:
* Adds a DevTools feature flag for named hooks support. (This allows us to disable it entirely for a build via feature flag.)
* Adds a new Suspense cache for dynamically imported modules. (This allows a component to suspend while importing an external code chunk– like the hook names parsing code).
* DevTools supports a hookNamesModuleLoaderFunction param to import the hook names module. I wish this could be handles as part of the react-devtools-shared package, but I'm not sure how to configure Webpack (4) to serve the chunk from react-devtools-inline. This seemed like a reasonable workaround.
The PR also contains an additional unrelated change:
* Removes pre-fetch optimization (added in DevTools: Improve named hooks network caching #22198). This optimization was mostly only important for cases where sources needed to be re-downloaded, something which we can now avoid in most cases¹ thanks to using cached responses already loaded by the page. (I tested this locally on Facebook and this change has no negative performance impact. There is still some overhead from serializing the JS through the Bridge but that's constant between the two approaches.)
¹ The case where we don't benefit from cached responses is when DevTools are opened after the page has already loaded certain scripts. This seems uncommon enough that I don't think it justified the added complexity of prefetching.
While testing the recently-launched named hooks feature, I noticed that one of the two big performance bottlenecks is fetching the source file. This was unexpected since the source file has already been loaded by the page. (After all, DevTools is inspecting a component defined in that same file.)
To address this, I made the following changes:
- [x] Keep CPU bound work (parsing source map and AST) in a worker so it doesn't block the main thread but move I/O bound code (fetching files) to the main thread.
- [x] Inject a function into the page (as part of the content script) to fetch cached files for the extension. Communicate with this function using `eval()` (to send it messages) and `chrome.runtime.sendMessage()` to return its responses to the extension).
With the above changes in place, the extension gets cached responses from a lot of sites- but not Facebook. This seems to be due to the following:
* Facebook's response headers include [`vary: 'Origin'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary).
* The `fetch` made from the content script does not include an `Origin` request header.
To reduce the impact of cases where we can't re-use the Network cache, this PR also makes additional changes:
- [x] Use `devtools.network.onRequestFinished` to (pre)cache resources as the page loads them. This allows us to avoid requesting a resource that's already been loaded in most cases.
- [x] In case DevTools was opened _after_ some requests were made, we also now pre-fetch (and cache in memory) source files when a component is selected (if it has hooks). If the component's hooks are later evaluated, the source map will be faster to access. (Note that in many cases, this prefetch is very fast since it is returned from the disk cache.)
With the above changes, we've reduced the time spent in `loadSourceFiles` to nearly nothing.
React currently suppress console logs in StrictMode during double rendering. However, this causes a lot of confusion. This PR moves the console suppression logic from React into React Devtools. Now by default, we no longer suppress console logs. Instead, we gray out the logs in console during double render. We also add a setting in React Devtools to allow developers to hide console logs during double render if they choose.
## Summary
Our current logic for extracting source map urls assumed that the url contained no query params (e.g. `?foo=bar`), and when extracting the url we would cut off the query params. I noticed this during internal testing, since removing the query params would cause loading source maps to fail.
This commit fixes that behavior by ensuring that our regex captures the full url, including query params.
## Test Plan
- yarn flow
- yarn test
- yarn test-build-devtools
- added new regression tests
- named hooks still work on manual test of browser extension on a few different apps (code sandbox, create-react-app, internally).
## Summary
Before this commit, if a hook returned an array the was destructured, but without assigning a variable to the first element in the array, this would produce an error. This was detected via internal testing.
This commit fixes that and adds regression tests.
## Test Plan
- yarn flow
- yarn test
- yarn test-build-devtools
- added new regression tests
- named hooks still work on manual test of browser extension on a few different apps (code sandbox, create-react-app, internally).
## Summary
Before this commit, if a hook returned an object and we declared a variable using object destructuring on the returned value, we would produce a runtime error. This was detected via internal testing.
This commit fixes that and adds regression tests.
## Test Plan
- yarn flow
- yarn test
- yarn test-build-devtools
- added new regression tests
- named hooks still work on manual test of browser extension on a few different apps (code sandbox, create-react-app, internally).