From 4f604941569d2e8947ce1460a0b2997e835f37b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 30 Aug 2024 15:11:57 -0400 Subject: [PATCH 1/5] [Flight] Ship DEV-only enableServerComponentLogs flag in Stable/Canary (#30847) To recap. This only affects DEV and RSC. It patches console on the server in DEV (similar to how React DevTools already does and what we did for the double logging). Then replays those logs with a `[Server]` badge on the client so you don't need a server terminal open. This has been on for over 6 months now in our experimental channel and we've had a lot of coverage in Next.js due to various experimental flags like taint and ppr. It's non-invasive in that even if something throws we just serialize that as an unknown value. The main feedback we've gotten was: - The serialization depth wasn't deep enough which I addressed in #30294 and haven't really had any issues since. This could still be an issue or the inverse that you serialize too many logs that are also too deep. This is not so much an issue with intentional logging and things like accidental errors don't typically have unbounded arguments (e.g. React errors are always string arguments). The ideal would be some way to retain objects and then load them on-demand but that needs more plumbing. Which can be later. - The other was that double logging on the server is annoying if the same terminal does both the RSC render and SSR render which was addressed in #30207. It is now off by default in node/edge-builds of the client, on by default in browser builds. With the `replayConsole` option to either opt-in or out. We've reached a good spot now I think. These are better with `enableOwnerStacks` but that's a separate track and not needed. The only thing to document here, other than maybe that we're doing it, is the `replayConsole` option but that's part of the RSC renderers that themselves are not documented so nowhere to document it. --- packages/shared/ReactFeatureFlags.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 673ff9d7e8450..10084be3cf2ef 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -134,7 +134,7 @@ export const alwaysThrottleRetries = true; export const passChildrenWhenCloningPersistedNodes = false; -export const enableServerComponentLogs = __EXPERIMENTAL__; +export const enableServerComponentLogs = true; /** * Enables a new Fiber flag used in persisted mode to reduce the number From 04ec50efa941a7f07e8231a87e72d6d851948b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 3 Sep 2024 12:29:15 -0400 Subject: [PATCH 2/5] [DevTools] Add Filtering of Environment Names (#30850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #30842. This adds a filter to be able to exclude Components from a certain environment. Default to Client or Server. The available options are computed into a dropdown based on the names that are currently used on the page (or an option that were previously used). In addition to the hardcoded "Client". Meaning that if you have Server Components on the page you see "Server" or "Client" as possible options but it can be anything if there are multiple RSC environments on the page. "Client" in this case means Function and Class Components in Fiber - excluding built-ins. If a Server Component has two environments (primary and secondary) then both have to be filtered to exclude it. We don't show the option at all if there are no Server Components used in the page to avoid confusing existing users that are just using Client Components and wouldn't know the difference between Server vs Client. Screenshot 2024-08-30 at 12 56 42 AM --- .../src/__tests__/utils.js | 13 +++ .../src/backend/agent.js | 19 ++++ .../src/backend/fiber/renderer.js | 80 +++++++++++++--- .../src/backend/legacy/renderer.js | 6 ++ .../src/backend/types.js | 1 + packages/react-devtools-shared/src/bridge.js | 2 + .../views/Settings/ComponentsSettings.js | 93 ++++++++++++++++++- .../devtools/views/Settings/SettingsModal.js | 5 +- .../views/Settings/SettingsModalContext.js | 47 ++++++++-- .../src/frontend/types.js | 13 ++- 10 files changed, 253 insertions(+), 26 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/utils.js b/packages/react-devtools-shared/src/__tests__/utils.js index 4a42cf2703ffb..c22ac6e05dc11 100644 --- a/packages/react-devtools-shared/src/__tests__/utils.js +++ b/packages/react-devtools-shared/src/__tests__/utils.js @@ -284,6 +284,19 @@ export function createHOCFilter(isEnabled: boolean = true) { }; } +export function createEnvironmentNameFilter( + env: string, + isEnabled: boolean = true, +) { + const Types = require('react-devtools-shared/src/frontend/types'); + return { + type: Types.ComponentFilterEnvironmentName, + isEnabled, + isValid: true, + value: env, + }; +} + export function createElementTypeFilter( elementType: ElementType, isEnabled: boolean = true, diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index a71b259441e98..9de4115b21d97 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -220,6 +220,7 @@ export default class Agent extends EventEmitter<{ this.updateConsolePatchSettings, ); bridge.addListener('updateComponentFilters', this.updateComponentFilters); + bridge.addListener('getEnvironmentNames', this.getEnvironmentNames); // Temporarily support older standalone front-ends sending commands to newer embedded backends. // We do this because React Native embeds the React DevTools backend, @@ -814,6 +815,24 @@ export default class Agent extends EventEmitter<{ } }; + getEnvironmentNames: () => void = () => { + let accumulatedNames = null; + for (const rendererID in this._rendererInterfaces) { + const renderer = this._rendererInterfaces[+rendererID]; + const names = renderer.getEnvironmentNames(); + if (accumulatedNames === null) { + accumulatedNames = names; + } else { + for (let i = 0; i < names.length; i++) { + if (accumulatedNames.indexOf(names[i]) === -1) { + accumulatedNames.push(names[i]); + } + } + } + } + this._bridge.send('environmentNames', accumulatedNames || []); + }; + onTraceUpdates: (nodes: Set) => void = nodes => { this.emit('traceUpdates', nodes); }; diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 47cb12bf17ae0..7960f68fb54a3 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -14,6 +14,7 @@ import { ComponentFilterElementType, ComponentFilterHOC, ComponentFilterLocation, + ComponentFilterEnvironmentName, ElementTypeClass, ElementTypeContext, ElementTypeFunction, @@ -721,6 +722,11 @@ export function getInternalReactConstants(version: string): { }; } +// All environment names we've seen so far. This lets us create a list of filters to apply. +// This should ideally include env of filtered Components too so that you can add those as +// filters at the same time as removing some other filter. +const knownEnvironmentNames: Set = new Set(); + // Map of one or more Fibers in a pair to their unique id number. // We track both Fibers to support Fast Refresh, // which may forcefully replace one of the pair as part of hot reloading. @@ -1099,6 +1105,7 @@ export function attach( const hideElementsWithDisplayNames: Set = new Set(); const hideElementsWithPaths: Set = new Set(); const hideElementsWithTypes: Set = new Set(); + const hideElementsWithEnvs: Set = new Set(); // Highlight updates let traceUpdatesEnabled: boolean = false; @@ -1108,6 +1115,7 @@ export function attach( hideElementsWithTypes.clear(); hideElementsWithDisplayNames.clear(); hideElementsWithPaths.clear(); + hideElementsWithEnvs.clear(); componentFilters.forEach(componentFilter => { if (!componentFilter.isEnabled) { @@ -1133,6 +1141,9 @@ export function attach( case ComponentFilterHOC: hideElementsWithDisplayNames.add(new RegExp('\\(')); break; + case ComponentFilterEnvironmentName: + hideElementsWithEnvs.add(componentFilter.value); + break; default: console.warn( `Invalid component filter type "${componentFilter.type}"`, @@ -1215,7 +1226,14 @@ export function attach( flushPendingEvents(); } - function shouldFilterVirtual(data: ReactComponentInfo): boolean { + function getEnvironmentNames(): Array { + return Array.from(knownEnvironmentNames); + } + + function shouldFilterVirtual( + data: ReactComponentInfo, + secondaryEnv: null | string, + ): boolean { // For purposes of filtering Server Components are always Function Components. // Environment will be used to filter Server vs Client. // Technically they can be forwardRef and memo too but those filters will go away @@ -1236,6 +1254,14 @@ export function attach( } } + if ( + (data.env == null || hideElementsWithEnvs.has(data.env)) && + (secondaryEnv === null || hideElementsWithEnvs.has(secondaryEnv)) + ) { + // If a Component has two environments, you have to filter both for it not to appear. + return true; + } + return false; } @@ -1294,6 +1320,26 @@ export function attach( } } + if (hideElementsWithEnvs.has('Client')) { + // If we're filtering out the Client environment we should filter out all + // "Client Components". Technically that also includes the built-ins but + // since that doesn't actually include any additional code loading it's + // useful to not filter out the built-ins. Those can be filtered separately. + // There's no other way to filter out just Function components on the Client. + // Therefore, this only filters Class and Function components. + switch (tag) { + case ClassComponent: + case IncompleteClassComponent: + case IncompleteFunctionComponent: + case FunctionComponent: + case IndeterminateComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: + return true; + } + } + /* DISABLED: https://github.com/facebook/react/pull/28417 if (hideElementsWithPaths.size > 0) { const source = getSourceForFiber(fiber); @@ -2489,7 +2535,14 @@ export function attach( } // Scan up until the next Component to see if this component changed environment. const componentInfo: ReactComponentInfo = (debugEntry: any); - if (shouldFilterVirtual(componentInfo)) { + const secondaryEnv = getSecondaryEnvironmentName(fiber._debugInfo, i); + if (componentInfo.env != null) { + knownEnvironmentNames.add(componentInfo.env); + } + if (secondaryEnv !== null) { + knownEnvironmentNames.add(secondaryEnv); + } + if (shouldFilterVirtual(componentInfo, secondaryEnv)) { // Skip. continue; } @@ -2511,10 +2564,6 @@ export function attach( ); } previousVirtualInstance = createVirtualInstance(componentInfo); - const secondaryEnv = getSecondaryEnvironmentName( - fiber._debugInfo, - i, - ); recordVirtualMount( previousVirtualInstance, reconcilingParent, @@ -2919,7 +2968,17 @@ export function attach( continue; } const componentInfo: ReactComponentInfo = (debugEntry: any); - if (shouldFilterVirtual(componentInfo)) { + const secondaryEnv = getSecondaryEnvironmentName( + nextChild._debugInfo, + i, + ); + if (componentInfo.env != null) { + knownEnvironmentNames.add(componentInfo.env); + } + if (secondaryEnv !== null) { + knownEnvironmentNames.add(secondaryEnv); + } + if (shouldFilterVirtual(componentInfo, secondaryEnv)) { continue; } if (level === virtualLevel) { @@ -2983,10 +3042,6 @@ export function attach( } else { // Otherwise we create a new instance. const newVirtualInstance = createVirtualInstance(componentInfo); - const secondaryEnv = getSecondaryEnvironmentName( - nextChild._debugInfo, - i, - ); recordVirtualMount( newVirtualInstance, reconcilingParent, @@ -3925,7 +3980,7 @@ export function attach( owner = ownerFiber._debugOwner; } else { const ownerInfo: ReactComponentInfo = (owner: any); // Refined - if (!shouldFilterVirtual(ownerInfo)) { + if (!shouldFilterVirtual(ownerInfo, null)) { return ownerInfo; } owner = ownerInfo.owner; @@ -5750,5 +5805,6 @@ export function attach( storeAsGlobal, unpatchConsoleForStrictMode, updateComponentFilters, + getEnvironmentNames, }; } diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 523fbbeba3d8e..ccff5ef07a1b6 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -1078,6 +1078,11 @@ export function attach( // Not implemented. } + function getEnvironmentNames(): Array { + // No RSC support. + return []; + } + function setTraceUpdatesEnabled(enabled: boolean) { // Not implemented. } @@ -1152,5 +1157,6 @@ export function attach( storeAsGlobal, unpatchConsoleForStrictMode, updateComponentFilters, + getEnvironmentNames, }; } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 41c278d02ebdf..2f1482fb1cbd2 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -416,6 +416,7 @@ export type RendererInterface = { ) => void, unpatchConsoleForStrictMode: () => void, updateComponentFilters: (componentFilters: Array) => void, + getEnvironmentNames: () => Array, // Timeline profiler interface diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index f4da08be6bdbe..1e9b3c222d623 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -189,6 +189,7 @@ export type BackendEvents = { operations: [Array], ownersList: [OwnersList], overrideComponentFilters: [Array], + environmentNames: [Array], profilingData: [ProfilingDataBackend], profilingStatus: [boolean], reloadAppForProfiling: [], @@ -237,6 +238,7 @@ type FrontendEvents = { stopProfiling: [], storeAsGlobal: [StoreAsGlobalParams], updateComponentFilters: [Array], + getEnvironmentNames: [], updateConsolePatchSettings: [ConsolePatchSettings], viewAttributeSource: [ViewAttributeSourceParams], viewElementSource: [ElementAndRendererID], diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js index a2a6a1d681819..33552daa262d8 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js @@ -15,6 +15,7 @@ import { useMemo, useRef, useState, + use, } from 'react'; import { LOCAL_STORAGE_OPEN_IN_EDITOR_URL, @@ -31,6 +32,7 @@ import { ComponentFilterElementType, ComponentFilterHOC, ComponentFilterLocation, + ComponentFilterEnvironmentName, ElementTypeClass, ElementTypeContext, ElementTypeFunction, @@ -52,11 +54,16 @@ import type { ElementType, ElementTypeComponentFilter, RegExpComponentFilter, + EnvironmentNameComponentFilter, } from 'react-devtools-shared/src/frontend/types'; const vscodeFilepath = 'vscode://file/{path}:{line}'; -export default function ComponentsSettings(_: {}): React.Node { +export default function ComponentsSettings({ + environmentNames, +}: { + environmentNames: Promise>, +}): React.Node { const store = useContext(StoreContext); const {parseHookNames, setParseHookNames} = useContext(SettingsContext); @@ -101,6 +108,30 @@ export default function ComponentsSettings(_: {}): React.Node { Array, >(() => [...store.componentFilters]); + const usedEnvironmentNames = use(environmentNames); + + const resolvedEnvironmentNames = useMemo(() => { + const set = new Set(usedEnvironmentNames); + // If there are other filters already specified but are not currently + // on the page, we still allow them as options. + for (let i = 0; i < componentFilters.length; i++) { + const filter = componentFilters[i]; + if (filter.type === ComponentFilterEnvironmentName) { + set.add(filter.value); + } + } + // Client is special and is always available as a default. + if (set.size > 0) { + // Only show any options at all if there's any other option already + // used by a filter or if any environments are used by the page. + // Note that "Client" can have been added above which would mean + // that we should show it as an option regardless if it's the only + // option. + set.add('Client'); + } + return Array.from(set).sort(); + }, [usedEnvironmentNames, componentFilters]); + const addFilter = useCallback(() => { setComponentFilters(prevComponentFilters => { return [ @@ -146,6 +177,13 @@ export default function ComponentsSettings(_: {}): React.Node { isEnabled: componentFilter.isEnabled, isValid: true, }; + } else if (type === ComponentFilterEnvironmentName) { + cloned[index] = { + type: ComponentFilterEnvironmentName, + isEnabled: componentFilter.isEnabled, + isValid: true, + value: 'Client', + }; } } return cloned; @@ -210,6 +248,29 @@ export default function ComponentsSettings(_: {}): React.Node { [], ); + const updateFilterValueEnvironmentName = useCallback( + (componentFilter: ComponentFilter, value: string) => { + if (componentFilter.type !== ComponentFilterEnvironmentName) { + throw Error('Invalid value for environment name filter'); + } + + setComponentFilters(prevComponentFilters => { + const cloned: Array = [...prevComponentFilters]; + if (componentFilter.type === ComponentFilterEnvironmentName) { + const index = prevComponentFilters.indexOf(componentFilter); + if (index >= 0) { + cloned[index] = { + ...componentFilter, + value, + }; + } + } + return cloned; + }); + }, + [], + ); + const removeFilter = useCallback((index: number) => { setComponentFilters(prevComponentFilters => { const cloned: Array = [...prevComponentFilters]; @@ -246,6 +307,11 @@ export default function ComponentsSettings(_: {}): React.Node { ...((cloned[index]: any): BooleanComponentFilter), isEnabled, }; + } else if (componentFilter.type === ComponentFilterEnvironmentName) { + cloned[index] = { + ...((cloned[index]: any): EnvironmentNameComponentFilter), + isEnabled, + }; } } return cloned; @@ -380,10 +446,16 @@ export default function ComponentsSettings(_: {}): React.Node { + {resolvedEnvironmentNames.length > 0 && ( + + )} - {componentFilter.type === ComponentFilterElementType && + {(componentFilter.type === ComponentFilterElementType || + componentFilter.type === ComponentFilterEnvironmentName) && 'equals'} {(componentFilter.type === ComponentFilterLocation || componentFilter.type === ComponentFilterDisplayName) && @@ -428,6 +500,23 @@ export default function ComponentsSettings(_: {}): React.Node { value={componentFilter.value} /> )} + {componentFilter.type === ComponentFilterEnvironmentName && ( + + )}