Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c68c046051 | ||
|
|
d651f69bc1 | ||
|
|
853550e7c8 | ||
|
|
5cf71b322d | ||
|
|
f807ce6492 | ||
|
|
7b38acca0b | ||
|
|
1d9c3927ea |
49
.github/workflows/devtools_discord_notify.yml
vendored
49
.github/workflows/devtools_discord_notify.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: (DevTools) Discord Notify
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
paths:
|
||||
- packages/react-devtools**
|
||||
- .github/workflows/devtools_**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check_access:
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
check_maintainer:
|
||||
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
|
||||
needs: [check_access]
|
||||
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
|
||||
permissions:
|
||||
# Used by check_maintainer
|
||||
contents: read
|
||||
with:
|
||||
actor: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
notify:
|
||||
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
|
||||
needs: check_maintainer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DEVTOOLS_DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: ${{ github.event.pull_request.user.login }}
|
||||
embed-author-url: ${{ github.event.pull_request.user.html_url }}
|
||||
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
|
||||
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
|
||||
embed-description: ${{ github.event.pull_request.body }}
|
||||
embed-url: ${{ github.event.pull_request.html_url }}
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools
|
||||
path: build/devtools
|
||||
path: build/devtools.tgz
|
||||
if-no-files-found: error
|
||||
# Simplifies getting the extension for local testing
|
||||
- name: Archive chrome extension
|
||||
@@ -201,5 +201,5 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: screenshots
|
||||
path: ./tmp/playwright-artifacts
|
||||
path: ./tmp/screenshots
|
||||
if-no-files-found: warn
|
||||
|
||||
13
.github/workflows/runtime_build_and_test.yml
vendored
13
.github/workflows/runtime_build_and_test.yml
vendored
@@ -766,11 +766,6 @@ jobs:
|
||||
name: react-devtools-${{ matrix.browser }}-extension
|
||||
path: build/devtools/${{ matrix.browser }}-extension.zip
|
||||
if-no-files-found: error
|
||||
- name: Archive ${{ matrix.browser }} metadata
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools-${{ matrix.browser }}-metadata
|
||||
path: build/devtools/webpack-stats.*.json
|
||||
|
||||
merge_devtools_artifacts:
|
||||
name: Merge DevTools artifacts
|
||||
@@ -781,7 +776,7 @@ jobs:
|
||||
uses: actions/upload-artifact/merge@v4
|
||||
with:
|
||||
name: react-devtools
|
||||
pattern: react-devtools-*
|
||||
pattern: react-devtools-*-extension
|
||||
|
||||
run_devtools_e2e_tests:
|
||||
name: Run DevTools e2e tests
|
||||
@@ -831,12 +826,6 @@ jobs:
|
||||
- run: ./scripts/ci/run_devtools_e2e_tests.js
|
||||
env:
|
||||
RELEASE_CHANNEL: experimental
|
||||
- name: Archive Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: devtools-playwright-artifacts
|
||||
path: tmp/playwright-artifacts
|
||||
if-no-files-found: warn
|
||||
|
||||
# ----- SIZEBOT -----
|
||||
sizebot:
|
||||
|
||||
2
.github/workflows/runtime_discord_notify.yml
vendored
2
.github/workflows/runtime_discord_notify.yml
vendored
@@ -4,10 +4,8 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
paths-ignore:
|
||||
- packages/react-devtools**
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_**.yml
|
||||
- .github/workflows/devtools**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,7 +23,6 @@ chrome-user-data
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
/tmp
|
||||
|
||||
packages/react-devtools-core/dist
|
||||
packages/react-devtools-extensions/chrome/build
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"all"
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Resizable} from 're-resizable';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
type TabsRecord = Map<string, React.ReactNode>;
|
||||
|
||||
export default function AccordionWindow(props: {
|
||||
defaultTab: string | null;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
}): React.ReactElement {
|
||||
if (props.tabs.size === 0) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{width: 'calc(100vw - 650px)'}}>
|
||||
No compiler output detected, see errors below
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-row h-full">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<AccordionWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionWindowItem({
|
||||
name,
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
}): React.ReactElement {
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const toggleTabs = useCallback(() => {
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
}, [tabsOpen, name, setTabsOpen]);
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,51 +6,83 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {PluginOptions} from 'babel-plugin-react-compiler';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import React, {useState} from 'react';
|
||||
import React, {useState, useCallback} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {
|
||||
ConfigError,
|
||||
generateOverridePragmaFromConfig,
|
||||
updateSourceWithOverridePragma,
|
||||
} from '../../lib/configUtils';
|
||||
|
||||
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
|
||||
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor({
|
||||
appliedOptions,
|
||||
}: {
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): React.ReactElement {
|
||||
export default function ConfigEditor(): React.ReactElement {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return isExpanded ? (
|
||||
<ExpandedEditor onToggle={setIsExpanded} appliedOptions={appliedOptions} />
|
||||
) : (
|
||||
<CollapsedEditor onToggle={setIsExpanded} />
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedEditor({
|
||||
onToggle,
|
||||
appliedOptions,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): React.ReactElement {
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setIsExpanded(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const handleApplyConfig: () => Promise<void> = async () => {
|
||||
try {
|
||||
const config = store.config || '';
|
||||
|
||||
if (!config.trim()) {
|
||||
enqueueSnackbar(
|
||||
'Config is empty. Please add configuration options first.',
|
||||
{
|
||||
variant: 'warning',
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
const newPragma = await generateOverridePragmaFromConfig(config);
|
||||
const updatedSource = updateSourceWithOverridePragma(
|
||||
store.source,
|
||||
newPragma,
|
||||
);
|
||||
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: updatedSource,
|
||||
config: config,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to apply config:', error);
|
||||
|
||||
if (error instanceof ConfigError && error.message.trim()) {
|
||||
enqueueSnackbar(error.message, {
|
||||
variant: 'error',
|
||||
});
|
||||
} else {
|
||||
enqueueSnackbar('Unexpected error: failed to apply config.', {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange: (value: string | undefined) => void = value => {
|
||||
if (value === undefined) return;
|
||||
|
||||
// Only update the config
|
||||
dispatchStore({
|
||||
type: 'updateConfig',
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: store.source,
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
@@ -85,113 +117,67 @@ function ExpandedEditor({
|
||||
}
|
||||
};
|
||||
|
||||
const formattedAppliedOptions = appliedOptions
|
||||
? prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
})
|
||||
: 'Invalid configs';
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350}}
|
||||
enable={{right: true, bottom: false}}>
|
||||
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
|
||||
<div
|
||||
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
|
||||
title="Minimize config editor"
|
||||
onClick={() => onToggle(false)}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
right: '-32px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="left" className="text-blue-50" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col m-2 mb-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Config Overrides
|
||||
<div className="flex flex-row relative">
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<Resizable
|
||||
className="border-r"
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350, height: 'auto'}}
|
||||
enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize config editor"
|
||||
aria-label="Minimize config editor"
|
||||
onClick={toggleExpanded}
|
||||
className="p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 font-light text-secondary hover:text-link">
|
||||
- Config Overrides
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-[calc(100vh_-_3.5rem_-_4rem)]">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
folding: false,
|
||||
renderLineHighlight: 'none',
|
||||
scrollBeyondLastLine: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Resizable>
|
||||
<button
|
||||
onClick={handleApplyConfig}
|
||||
title="Apply config overrides to input"
|
||||
aria-label="Apply config overrides to input"
|
||||
className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10 w-8 h-8 bg-blue-500 hover:bg-blue-600 text-white rounded-full border-2 border-white shadow-lg flex items-center justify-center text-sm font-medium transition-colors duration-150">
|
||||
→
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title="Expand config editor"
|
||||
aria-label="Expand config editor"
|
||||
style={{
|
||||
transform: 'rotate(90deg) translate(-50%)',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
onClick={toggleExpanded}
|
||||
className="flex-grow-0 w-5 transition-colors duration-150 ease-in font-light text-secondary hover:text-link">
|
||||
Config Overrides
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col m-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Applied Configs
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedOptions}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
readOnly: true,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsedEditor({
|
||||
onToggle,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className="w-4 !h-[calc(100vh_-_3.5rem)]"
|
||||
style={{position: 'relative'}}>
|
||||
<div
|
||||
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
|
||||
title="Expand config editor"
|
||||
onClick={() => onToggle(true)}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
left: '-8px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="right" className="text-blue-50" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import BabelPluginReactCompiler, {
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import clsx from 'clsx';
|
||||
import invariant from 'invariant';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {useDeferredValue, useMemo} from 'react';
|
||||
@@ -46,6 +46,8 @@ import {
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
import {useSearchParams} from 'next/navigation';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -142,66 +144,10 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
function parseOptions(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): PluginOptions {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
|
||||
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
|
||||
// Parse config overrides from config editor
|
||||
let configOverrideOptions: any = {};
|
||||
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
|
||||
// TODO: initialize store with URL params, not empty store
|
||||
if (configOverrides.trim()) {
|
||||
if (configMatch && configMatch[1]) {
|
||||
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
|
||||
configOverrideOptions = new Function(`return (${configString})`)();
|
||||
} else {
|
||||
throw new Error('Invalid override format');
|
||||
}
|
||||
}
|
||||
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedPragmaOptions,
|
||||
...configOverrideOptions,
|
||||
environment: {
|
||||
...parsedPragmaOptions.environment,
|
||||
...configOverrideOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
});
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
@@ -220,94 +166,104 @@ function compile(
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
try {
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} catch (err) {
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Config,
|
||||
reason: `Unexpected failure when transforming configs! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (baseOpts) {
|
||||
try {
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
};
|
||||
// Add logger options to the parsed options
|
||||
const opts = {
|
||||
...baseOpts,
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: `Unexpected failure when transforming input! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
const parsedOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedOptions,
|
||||
environment: {
|
||||
...parsedOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent) => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
console.error(err);
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: `Unexpected failure when transforming input! ${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
@@ -315,12 +271,11 @@ function compile(
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
return [{kind: 'err', results, error}, language];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -329,15 +284,20 @@ export default function Editor(): JSX.Element {
|
||||
const deferredStore = useDeferredValue(store);
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [compilerOutput, language, appliedOptions] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
const [compilerOutput, language] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler'),
|
||||
[deferredStore.source],
|
||||
);
|
||||
const [linterOutput] = useMemo(
|
||||
() => compile(deferredStore.source, 'linter', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
() => compile(deferredStore.source, 'linter'),
|
||||
[deferredStore.source],
|
||||
);
|
||||
|
||||
// TODO: Remove this once the config editor is more stable
|
||||
const searchParams = useSearchParams();
|
||||
const search = searchParams.get('showConfig');
|
||||
const shouldShowConfig = search === 'true';
|
||||
|
||||
useMountEffect(() => {
|
||||
// Initialize store
|
||||
let mountStore: Store;
|
||||
@@ -378,17 +338,13 @@ export default function Editor(): JSX.Element {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex top-14">
|
||||
<div className="flex-shrink-0">
|
||||
<ConfigEditor appliedOptions={appliedOptions} />
|
||||
<div className="relative flex basis top-14">
|
||||
{shouldShowConfig && <ConfigEditor />}
|
||||
<div className={clsx('relative sm:basis-1/4')}>
|
||||
<Input language={language} errors={errors} />
|
||||
</div>
|
||||
<div className="flex flex-1 min-w-0">
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Input language={language} errors={errors} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
<div className={clsx('flex sm:flex flex-wrap')}>
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
|
||||
import invariant from 'invariant';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
@@ -17,15 +14,15 @@ import {Resizable} from 're-resizable';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
|
||||
import React$Types from '../../node_modules/@types/react/index.d.ts';
|
||||
import {parseAndFormatConfig} from '../../lib/configUtils.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
type Props = {
|
||||
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
errors: Array<CompilerErrorDetail>;
|
||||
language: 'flow' | 'typescript';
|
||||
};
|
||||
|
||||
@@ -86,10 +83,14 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
const handleChange: (value: string | undefined) => void = async value => {
|
||||
if (!value) return;
|
||||
|
||||
// Parse and format the config
|
||||
const config = await parseAndFormatConfig(value);
|
||||
|
||||
dispatchStore({
|
||||
type: 'updateSource',
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: value,
|
||||
config,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -139,51 +140,30 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const editorContent = (
|
||||
<MonacoEditor
|
||||
path={'index.js'}
|
||||
/**
|
||||
* .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
* check their syntax using its TS language service. They are still JS files
|
||||
* due to their extensions, so TS language features don't work.
|
||||
*/
|
||||
language={'javascript'}
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={monacoOptions}
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = new Map([['Input', editorContent]]);
|
||||
const [activeTab, setActiveTab] = useState('Input');
|
||||
|
||||
const tabbedContent = (
|
||||
<div className="flex flex-col h-full">
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col flex-none border-r border-gray-200">
|
||||
{store.showInternals ? (
|
||||
<Resizable
|
||||
minWidth={550}
|
||||
enable={{right: true}}
|
||||
<Resizable
|
||||
minWidth={650}
|
||||
enable={{right: true}}
|
||||
/**
|
||||
* Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
* will grow the editor to fit within parent element
|
||||
*/
|
||||
className="!h-[calc(100vh_-_3.5rem)]">
|
||||
<MonacoEditor
|
||||
path={'index.js'}
|
||||
/**
|
||||
* Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
* will grow the editor to fit within parent element
|
||||
* .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
* check their syntax using its TS language service. They are still JS files
|
||||
* due to their extensions, so TS language features don't work.
|
||||
*/
|
||||
className="!h-[calc(100vh_-_3.5rem)]">
|
||||
{tabbedContent}
|
||||
</Resizable>
|
||||
) : (
|
||||
<div className="!h-[calc(100vh_-_3.5rem)]">{tabbedContent}</div>
|
||||
)}
|
||||
language={'javascript'}
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={monacoOptions}
|
||||
/>
|
||||
</Resizable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,17 +21,13 @@ import * as prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {memo, ReactNode, useEffect, useState} from 'react';
|
||||
import {type Store} from '../../lib/stores';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
|
||||
const MemoizedOutput = memo(Output);
|
||||
|
||||
export default MemoizedOutput;
|
||||
|
||||
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'hir';
|
||||
@@ -75,7 +71,7 @@ async function tabify(
|
||||
const concattedResults = new Map<string, string>();
|
||||
// Concat all top level function declaration results into a single tab for each pass
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
|
||||
if (!showInternals && passName !== 'Output' && passName !== 'SourceMap') {
|
||||
continue;
|
||||
}
|
||||
for (const result of results) {
|
||||
@@ -219,7 +215,6 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<string>('Output');
|
||||
|
||||
/*
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
@@ -231,7 +226,6 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
setTabsOpen(new Set(['Output']));
|
||||
setActiveTab('Output');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -255,24 +249,16 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
if (!store.showInternals) {
|
||||
return (
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccordionWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
<>
|
||||
<TabbedWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function Header(): JSX.Element {
|
||||
'before:bg-white before:rounded-full before:transition-transform before:duration-250',
|
||||
'focus-within:shadow-[0_0_1px_#2196F3]',
|
||||
store.showInternals
|
||||
? 'bg-link before:translate-x-3.5'
|
||||
? 'bg-blue-500 before:translate-x-3.5'
|
||||
: 'bg-gray-300',
|
||||
)}></span>
|
||||
</label>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {memo} from 'react';
|
||||
|
||||
export const IconChevron = memo<
|
||||
JSX.IntrinsicElements['svg'] & {
|
||||
/**
|
||||
* The direction the arrow should point.
|
||||
*/
|
||||
displayDirection: 'right' | 'left';
|
||||
}
|
||||
>(function IconChevron({className, displayDirection, ...props}) {
|
||||
const rotationClass =
|
||||
displayDirection === 'left' ? 'rotate-90' : '-rotate-90';
|
||||
const classes = className ? `${rotationClass} ${className}` : rotationClass;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={classes}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
{...props}>
|
||||
<g fill="none" fillRule="evenodd" transform="translate(-446 -398)">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
d="M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z"
|
||||
transform="translate(356.5 164.5)"
|
||||
/>
|
||||
<polygon points="446 418 466 418 466 398 446 398" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -53,14 +53,9 @@ type ReducerAction =
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateSource';
|
||||
type: 'updateFile';
|
||||
payload: {
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateConfig';
|
||||
payload: {
|
||||
config: string;
|
||||
};
|
||||
}
|
||||
@@ -74,18 +69,11 @@ function storeReducer(store: Store, action: ReducerAction): Store {
|
||||
const newStore = action.payload.store;
|
||||
return newStore;
|
||||
}
|
||||
case 'updateSource': {
|
||||
const source = action.payload.source;
|
||||
case 'updateFile': {
|
||||
const {source, config} = action.payload;
|
||||
const newStore = {
|
||||
...store,
|
||||
source,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
case 'updateConfig': {
|
||||
const config = action.payload.config;
|
||||
const newStore = {
|
||||
...store,
|
||||
config,
|
||||
};
|
||||
return newStore;
|
||||
|
||||
@@ -4,47 +4,103 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export default function TabbedWindow({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
}: {
|
||||
tabs: Map<string, React.ReactNode>;
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
import {Resizable} from 're-resizable';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
type TabsRecord = Map<string, React.ReactNode>;
|
||||
|
||||
export default function TabbedWindow(props: {
|
||||
defaultTab: string | null;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
}): React.ReactElement {
|
||||
if (tabs.size === 0) {
|
||||
if (props.tabs.size === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center flex-1 max-w-full">
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{width: 'calc(100vw - 650px)'}}>
|
||||
No compiler output detected, see errors below
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col h-full max-w-full">
|
||||
<div className="flex p-2 flex-shrink-0">
|
||||
{Array.from(tabs.keys()).map(tab => {
|
||||
const isActive = activeTab === tab;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => onTabChange(tab)}
|
||||
className={clsx(
|
||||
'active:scale-95 transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm',
|
||||
!isActive && 'hover:bg-primary/5',
|
||||
isActive && 'bg-highlight text-link',
|
||||
)}>
|
||||
{tab}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(activeTab)}
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<TabbedWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabbedWindowItem({
|
||||
name,
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
}): React.ReactElement {
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const toggleTabs = useCallback(() => {
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
}, [tabsOpen, name, setTabsOpen]);
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
120
compiler/apps/playground/lib/configUtils.ts
Normal file
120
compiler/apps/playground/lib/configUtils.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import parserBabel from 'prettier/plugins/babel';
|
||||
import prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {parsePluginOptions} from 'babel-plugin-react-compiler';
|
||||
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
|
||||
|
||||
export class ConfigError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ConfigError';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parse config from pragma and format it with prettier
|
||||
*/
|
||||
export async function parseAndFormatConfig(source: string): Promise<string> {
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
let configString = parseConfigPragmaAsString(pragma);
|
||||
if (configString !== '') {
|
||||
configString = `\
|
||||
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
(${configString} satisfies Partial<PluginOptions>)`;
|
||||
}
|
||||
|
||||
try {
|
||||
const formatted = await prettier.format(configString, {
|
||||
semi: true,
|
||||
parser: 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
return formatted;
|
||||
} catch (error) {
|
||||
console.error('Error formatting config:', error);
|
||||
return ''; // Return empty string if not valid for now
|
||||
}
|
||||
}
|
||||
|
||||
function extractCurlyBracesContent(input: string): string {
|
||||
const startIndex = input.indexOf('({') + 1;
|
||||
const endIndex = input.lastIndexOf('}');
|
||||
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
||||
throw new Error('No outer curly braces found in input.');
|
||||
}
|
||||
return input.slice(startIndex, endIndex + 1);
|
||||
}
|
||||
|
||||
function cleanContent(content: string): string {
|
||||
return content
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a config string can be parsed as a valid PluginOptions object
|
||||
* Throws an error if validation fails.
|
||||
*/
|
||||
function validateConfigAsPluginOptions(configString: string): void {
|
||||
// Validate that config can be parse as JS obj
|
||||
let parsedConfig: unknown;
|
||||
try {
|
||||
parsedConfig = new Function(`return (${configString})`)();
|
||||
} catch (_) {
|
||||
throw new ConfigError('Config has invalid syntax.');
|
||||
}
|
||||
|
||||
// Validate against PluginOptions schema
|
||||
try {
|
||||
parsePluginOptions(parsedConfig);
|
||||
} catch (_) {
|
||||
throw new ConfigError('Config does not match the expected schema.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a the override pragma comment from a formatted config object string
|
||||
*/
|
||||
export async function generateOverridePragmaFromConfig(
|
||||
formattedConfigString: string,
|
||||
): Promise<string> {
|
||||
const content = extractCurlyBracesContent(formattedConfigString);
|
||||
const cleanConfig = cleanContent(content);
|
||||
|
||||
validateConfigAsPluginOptions(cleanConfig);
|
||||
|
||||
// Format the config to ensure it's valid
|
||||
await prettier.format(`(${cleanConfig})`, {
|
||||
semi: false,
|
||||
parser: 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
|
||||
return `// @OVERRIDE:${cleanConfig}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the override pragma comment in source code.
|
||||
*/
|
||||
export function updateSourceWithOverridePragma(
|
||||
source: string,
|
||||
newPragma: string,
|
||||
): string {
|
||||
const firstLineEnd = source.indexOf('\n');
|
||||
const firstLine = source.substring(0, firstLineEnd);
|
||||
|
||||
const pragmaRegex = /^\/\/\s*@/;
|
||||
if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) {
|
||||
return newPragma + source.substring(firstLineEnd);
|
||||
} else {
|
||||
return newPragma + '\n' + source;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,22 @@ export const defaultConfig = `\
|
||||
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
//compilationMode: "all"
|
||||
compilationMode: 'infer',
|
||||
panicThreshold: 'none',
|
||||
environment: {},
|
||||
logger: null,
|
||||
gating: null,
|
||||
noEmit: false,
|
||||
dynamicGating: null,
|
||||
eslintSuppressionRules: null,
|
||||
flowSuppressions: true,
|
||||
ignoreUseNoForget: false,
|
||||
sources: filename => {
|
||||
return filename.indexOf('node_modules') === -1;
|
||||
},
|
||||
enableReanimatedCheck: true,
|
||||
customOptOutDirectives: null,
|
||||
target: '19',
|
||||
} satisfies Partial<PluginOptions>);`;
|
||||
|
||||
export const defaultStore: Store = {
|
||||
|
||||
@@ -55,16 +55,12 @@ export default defineConfig({
|
||||
// contextOptions: {
|
||||
// ignoreHTTPSErrors: true,
|
||||
// },
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
use: {...devices['Desktop Chrome']},
|
||||
},
|
||||
// {
|
||||
// name: 'Desktop Firefox',
|
||||
|
||||
@@ -276,7 +276,7 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir, env));
|
||||
env.logErrors(validateNoSetStateInEffects(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
|
||||
@@ -86,24 +86,6 @@ export function defaultModuleTypeProvider(
|
||||
},
|
||||
};
|
||||
}
|
||||
case '@tanstack/react-virtual': {
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
/*
|
||||
* Many of the properties of `useVirtualizer()`'s return value are incompatible, so we mark the entire hook
|
||||
* as incompatible
|
||||
*/
|
||||
useVirtualizer: {
|
||||
kind: 'hook',
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
knownIncompatible: `TanStack Virtual's \`useVirtualizer()\` API returns functions that cannot be memoized safely`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -621,13 +621,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Treat identifiers as SetState type if both
|
||||
* - they are named with a "set-" prefix
|
||||
* - they are called somewhere
|
||||
*/
|
||||
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* If specified a value, the compiler lowers any calls to `useContext` to use
|
||||
* this value as the callee.
|
||||
@@ -667,13 +660,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* while its parent function remains uncompiled.
|
||||
*/
|
||||
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When enabled, allows setState calls in effects when the value being set is
|
||||
* derived from a ref. This is useful for patterns where initial layout measurements
|
||||
* from refs need to be stored in state during mount.
|
||||
*/
|
||||
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
|
||||
@@ -748,14 +748,10 @@ function applyEffect(
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'MaybeAlias' ||
|
||||
initialized.has(effect.into.identifier.id),
|
||||
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
|
||||
{
|
||||
reason: `Expected destination to already be initialized within this instruction`,
|
||||
description:
|
||||
`Destination ${printPlace(effect.into)} is not initialized in this ` +
|
||||
`instruction for effect ${printAliasingEffect(effect)}`,
|
||||
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
|
||||
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
@@ -771,67 +767,49 @@ function applyEffect(
|
||||
* copy-on-write semantics, then we can prune the effect
|
||||
*/
|
||||
const intoKind = state.kind(effect.into).kind;
|
||||
let destinationType: 'context' | 'mutable' | null = null;
|
||||
let isMutableDesination: boolean;
|
||||
switch (intoKind) {
|
||||
case ValueKind.Context: {
|
||||
destinationType = 'context';
|
||||
break;
|
||||
}
|
||||
case ValueKind.Context:
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
destinationType = 'mutable';
|
||||
isMutableDesination = true;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableDesination = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fromKind = state.kind(effect.from).kind;
|
||||
let sourceType: 'context' | 'mutable' | 'frozen' | null = null;
|
||||
let isMutableReferenceType: boolean;
|
||||
switch (fromKind) {
|
||||
case ValueKind.Context: {
|
||||
sourceType = 'context';
|
||||
break;
|
||||
}
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Primitive: {
|
||||
isMutableReferenceType = false;
|
||||
break;
|
||||
}
|
||||
case ValueKind.Frozen: {
|
||||
sourceType = 'frozen';
|
||||
isMutableReferenceType = false;
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
sourceType = 'mutable';
|
||||
isMutableReferenceType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceType === 'frozen') {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
} else if (
|
||||
(sourceType === 'mutable' && destinationType === 'mutable') ||
|
||||
effect.kind === 'MaybeAlias'
|
||||
) {
|
||||
if (isMutableDesination && isMutableReferenceType) {
|
||||
effects.push(effect);
|
||||
} else if (
|
||||
(sourceType === 'context' && destinationType != null) ||
|
||||
(sourceType === 'mutable' && destinationType === 'context')
|
||||
) {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{kind: 'MaybeAlias', from: effect.from, into: effect.into},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1816,16 +1794,8 @@ function computeSignatureForInstruction(
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
/**
|
||||
* Add a hint about naming as "ref"/"-Ref", but only if we weren't able to infer any
|
||||
* type for the object. In some cases the variable may be named like a ref, but is
|
||||
* also used as a ref callback such that we infer the type as a function rather than
|
||||
* a ref.
|
||||
*/
|
||||
const mutationReason: MutationReason | null =
|
||||
value.kind === 'PropertyStore' &&
|
||||
value.property === 'current' &&
|
||||
value.object.identifier.type.kind === 'Type'
|
||||
value.kind === 'PropertyStore' && value.property === 'current'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
|
||||
@@ -779,13 +779,7 @@ class AliasingState {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({
|
||||
place: edge.node,
|
||||
transitive,
|
||||
direction: 'forwards',
|
||||
// Traversing a maybeAlias edge always downgrades to conditional mutation
|
||||
kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,
|
||||
});
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -813,12 +807,7 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
queue.push({place: alias, transitive, direction: 'backwards', kind});
|
||||
}
|
||||
/**
|
||||
* MaybeAlias indicates potential data flow from unknown function calls,
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
BuiltInObjectId,
|
||||
BuiltInPropsId,
|
||||
BuiltInRefValueId,
|
||||
BuiltInSetStateId,
|
||||
BuiltInUseRefId,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
@@ -277,16 +276,9 @@ function* generateInstructionTypes(
|
||||
* We should change Hook to a subtype of Function or change unifier logic.
|
||||
* (see https://github.com/facebook/react-forget/pull/1427)
|
||||
*/
|
||||
let shapeId: string | null = null;
|
||||
if (env.config.enableTreatSetIdentifiersAsStateSetters) {
|
||||
const name = getName(names, value.callee.identifier.id);
|
||||
if (name.startsWith('set')) {
|
||||
shapeId = BuiltInSetStateId;
|
||||
}
|
||||
}
|
||||
yield equation(value.callee.identifier.type, {
|
||||
kind: 'Function',
|
||||
shapeId,
|
||||
shapeId: null,
|
||||
return: returnType,
|
||||
isConstructor: false,
|
||||
});
|
||||
|
||||
@@ -188,6 +188,11 @@ export function parseConfigPragmaForTests(
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
const overridePragma = parseConfigPragmaAsString(pragma);
|
||||
if (overridePragma !== '') {
|
||||
return parseConfigStringAsJS(overridePragma, defaults);
|
||||
}
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
pragma,
|
||||
defaults.environment ?? {},
|
||||
@@ -223,3 +228,100 @@ export function parseConfigPragmaForTests(
|
||||
}
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
|
||||
export function parseConfigPragmaAsString(pragma: string): string {
|
||||
// Check if it's in JS override format
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (key === 'OVERRIDE' && val != null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function parseConfigStringAsJS(
|
||||
configString: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
let parsedConfig: any;
|
||||
try {
|
||||
// Parse the JavaScript object literal
|
||||
parsedConfig = new Function(`return ${configString}`)();
|
||||
} catch (error) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Failed to parse config pragma as JavaScript object',
|
||||
description: `Could not parse: ${configString}. Error: ${error}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
'',
|
||||
defaults.environment ?? {},
|
||||
);
|
||||
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
compilationMode: defaults.compilationMode,
|
||||
environment,
|
||||
};
|
||||
|
||||
// Apply parsed config, merging environment if it exists
|
||||
if (parsedConfig.environment) {
|
||||
const mergedEnvironment = {
|
||||
...(options.environment as Record<string, unknown>),
|
||||
...parsedConfig.environment,
|
||||
};
|
||||
|
||||
// Validate environment config
|
||||
const validatedEnvironment =
|
||||
EnvironmentConfigSchema.safeParse(mergedEnvironment);
|
||||
if (!validatedEnvironment.success) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Invalid environment configuration in config pragma',
|
||||
description: `${fromZodError(validatedEnvironment.error)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
options.environment = validatedEnvironment.data;
|
||||
}
|
||||
|
||||
// Apply other config options
|
||||
for (const [key, value] of Object.entries(parsedConfig)) {
|
||||
if (key === 'environment') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasOwnProperty(defaultOptions, key)) {
|
||||
if (key === 'target' && value === 'donotuse_meta_internal') {
|
||||
options[key] = {
|
||||
kind: value,
|
||||
runtimeModule: 'react',
|
||||
};
|
||||
} else {
|
||||
options[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
|
||||
@@ -5,21 +5,216 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {CompilerError, Effect, ErrorSeverity, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseStateType,
|
||||
GeneratedSource,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
eachInstructionLValue,
|
||||
} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
// TODO: Maybe I can consolidate some types
|
||||
type SetStateCall = {
|
||||
loc: SourceLocation;
|
||||
invalidDeps: DerivationMetadata;
|
||||
setStateId: IdentifierId;
|
||||
};
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
|
||||
|
||||
type SetStateName = string | undefined | null;
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sources: Array<Place>;
|
||||
};
|
||||
|
||||
type ErrorMetadata = {
|
||||
errorType: TypeOfValue;
|
||||
invalidDepInfo: string | undefined;
|
||||
loc: SourceLocation;
|
||||
setStateName: SetStateName;
|
||||
};
|
||||
|
||||
function joinValue(
|
||||
lvalueType: TypeOfValue,
|
||||
valueType: TypeOfValue,
|
||||
): TypeOfValue {
|
||||
if (lvalueType === 'ignored') return valueType;
|
||||
if (valueType === 'ignored') return lvalueType;
|
||||
if (lvalueType === valueType) return lvalueType;
|
||||
return 'fromPropsOrState';
|
||||
}
|
||||
|
||||
function updateDerivationMetadata(
|
||||
target: Place,
|
||||
sources: Array<DerivationMetadata>,
|
||||
typeOfValue: TypeOfValue,
|
||||
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: target,
|
||||
sources: [],
|
||||
typeOfValue: typeOfValue,
|
||||
};
|
||||
|
||||
for (const source of sources) {
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
if (source.place.identifier.name?.kind === 'promoted') {
|
||||
newValue.sources.push(target);
|
||||
} else {
|
||||
newValue.sources.push(...source.sources);
|
||||
}
|
||||
}
|
||||
derivedTuple.set(target.identifier.id, newValue);
|
||||
}
|
||||
|
||||
function parseInstr(
|
||||
instr: Instruction,
|
||||
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||
setStateCalls: Map<SetStateName, Array<Place>>,
|
||||
): void {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
|
||||
// TODO: Not sure if this will catch every time we create a new useState
|
||||
if (
|
||||
instr.value.kind === 'Destructure' &&
|
||||
instr.value.lvalue.pattern.kind === 'ArrayPattern' &&
|
||||
isUseStateType(instr.value.value.identifier)
|
||||
) {
|
||||
const value = instr.value.lvalue.pattern.items[0];
|
||||
if (value.kind === 'Identifier') {
|
||||
derivedTuple.set(value.identifier.id, {
|
||||
place: value,
|
||||
sources: [value],
|
||||
typeOfValue: 'fromState',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier' &&
|
||||
instr.value.callee.loc !== GeneratedSource
|
||||
) {
|
||||
if (setStateCalls.has(instr.value.callee.loc.identifierName)) {
|
||||
setStateCalls
|
||||
.get(instr.value.callee.loc.identifierName)!
|
||||
.push(instr.value.callee);
|
||||
} else {
|
||||
setStateCalls.set(instr.value.callee.loc.identifierName, [
|
||||
instr.value.callee,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
let sources: Array<DerivationMetadata> = [];
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const opSource = derivedTuple.get(operand.identifier.id);
|
||||
if (opSource === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, opSource.typeOfValue);
|
||||
sources.push(opSource);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
updateDerivationMetadata(lvalue, sources, typeOfValue, derivedTuple);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
switch (operand.effect) {
|
||||
case Effect.Capture:
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
if (isMutable(instr, operand)) {
|
||||
updateDerivationMetadata(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
derivedTuple,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: operand.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseBlockPhi(
|
||||
block: BasicBlock,
|
||||
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands.values()) {
|
||||
const source = derivedTuple.get(operand.identifier.id);
|
||||
if (source !== undefined && source.typeOfValue === 'fromProps') {
|
||||
if (
|
||||
source.place.identifier.name === null ||
|
||||
source.place.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
derivedTuple.set(phi.place.identifier.id, {
|
||||
place: phi.place,
|
||||
sources: [phi.place],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
} else {
|
||||
derivedTuple.set(phi.place.identifier.id, {
|
||||
place: phi.place,
|
||||
sources: source.sources,
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
@@ -48,12 +243,54 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const derivedTuple: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
const effectSetStates: Map<SetStateName, Array<Place>> = new Map();
|
||||
const setStateCalls: Map<SetStateName, Array<Place>> = new Map();
|
||||
|
||||
const errors: Array<ErrorMetadata> = [];
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
derivedTuple.set(param.identifier.id, {
|
||||
place: param,
|
||||
sources: [param],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
derivedTuple.set(props.identifier.id, {
|
||||
place: props,
|
||||
sources: [props],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
parseBlockPhi(block, derivedTuple);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
|
||||
parseInstr(instr, derivedTuple, setStateCalls);
|
||||
|
||||
/*
|
||||
* Special case for function expressions, we need to parse nested instructions
|
||||
* TODO: Can there be more recursive levels?
|
||||
*/
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
parseInstr(instr, derivedTuple, setStateCalls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
@@ -66,6 +303,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
@@ -97,6 +335,8 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
derivedTuple,
|
||||
effectSetStates,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
@@ -104,43 +344,98 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
|
||||
const throwableErrors = new CompilerError();
|
||||
for (const error of errors) {
|
||||
let reason;
|
||||
// TODO: Not sure if this is robust enough.
|
||||
/*
|
||||
* If we use a setState from an invalid useEffect elsewhere then we probably have to
|
||||
* hoist state up, else we should calculate in render
|
||||
*/
|
||||
if (
|
||||
setStateCalls.get(error.setStateName)?.length !=
|
||||
effectSetStates.get(error.setStateName)?.length &&
|
||||
error.errorType !== 'fromState'
|
||||
) {
|
||||
reason =
|
||||
'Consider lifting state up to the parent component to make this a controlled component. (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)';
|
||||
} else {
|
||||
reason =
|
||||
'You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)';
|
||||
}
|
||||
|
||||
throwableErrors.push({
|
||||
reason: reason,
|
||||
description: `You are using invalid dependencies: \n\n${error.invalidDepInfo}`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: error.loc,
|
||||
});
|
||||
}
|
||||
|
||||
if (throwableErrors.hasAnyErrors()) {
|
||||
throw throwableErrors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||
effectSetStates: Map<SetStateName, Array<Place>>,
|
||||
errors: Array<ErrorMetadata>,
|
||||
): void {
|
||||
/*
|
||||
* TODO: This makes it so we only capture single line useEffects.
|
||||
* We should be able to capture multiline as well
|
||||
*/
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else if (derivedTuple.has(operand.identifier.id)) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This might be wrong gotta double check
|
||||
let hasInvalidDep = false;
|
||||
for (const dep of effectDeps) {
|
||||
const depMetadata = derivedTuple.get(dep);
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) !=
|
||||
null ||
|
||||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
|
||||
) {
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
hasInvalidDep = true;
|
||||
}
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
if (!hasInvalidDep) {
|
||||
console.log('early return 2');
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
// This variable is suspicious maybe we don't need it?
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
const effectInvalidlyDerived: Map<IdentifierId, DerivationMetadata> =
|
||||
new Map();
|
||||
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
const depMetadata = derivedTuple.get(dep);
|
||||
if (depMetadata !== undefined) {
|
||||
effectInvalidlyDerived.set(dep, depMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
const setStateCallsInEffect: Array<SetStateCall> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -148,21 +443,29 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
|
||||
parseBlockPhi(block, effectInvalidlyDerived);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier' &&
|
||||
instr.value.callee.loc !== GeneratedSource &&
|
||||
instr.value.callee.loc.identifierName !== undefined &&
|
||||
instr.value.callee.loc.identifierName !== null
|
||||
) {
|
||||
if (effectSetStates.has(instr.value.callee.loc.identifierName)) {
|
||||
effectSetStates
|
||||
.get(instr.value.callee.loc.identifierName)!
|
||||
.push(instr.value.callee);
|
||||
} else {
|
||||
effectSetStates.set(instr.value.callee.loc.identifierName, [
|
||||
instr.value.callee,
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
@@ -183,7 +486,7 @@ function validateEffect(
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
@@ -201,38 +504,69 @@ function validateEffect(
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
const invalidDeps = derivedTuple.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (invalidDeps !== undefined) {
|
||||
setStateCallsInEffect.push({
|
||||
loc: instr.value.callee.loc,
|
||||
setStateId: instr.value.callee.identifier.id,
|
||||
invalidDeps: invalidDeps,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log('early return 4');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
for (const call of setStateCallsInEffect) {
|
||||
const placeNames = call.invalidDeps.sources
|
||||
.map(place => place.identifier.name?.value)
|
||||
.join(', ');
|
||||
|
||||
let sourceNames = '';
|
||||
let invalidDepInfo = '';
|
||||
console.log(call.invalidDeps);
|
||||
if (call.invalidDeps.typeOfValue === 'fromProps') {
|
||||
sourceNames += `[${placeNames}], `;
|
||||
sourceNames = sourceNames.slice(0, -2);
|
||||
invalidDepInfo = sourceNames
|
||||
? `Invalid deps from props ${sourceNames}`
|
||||
: '';
|
||||
} else if (call.invalidDeps.typeOfValue === 'fromState') {
|
||||
sourceNames += `[${placeNames}], `;
|
||||
sourceNames = sourceNames.slice(0, -2);
|
||||
invalidDepInfo = sourceNames
|
||||
? `Invalid deps from local state: ${sourceNames}`
|
||||
: '';
|
||||
} else {
|
||||
sourceNames += `[${placeNames}], `;
|
||||
sourceNames = sourceNames.slice(0, -2);
|
||||
invalidDepInfo = sourceNames
|
||||
? `Invalid deps from both props and local state: ${sourceNames}`
|
||||
: '';
|
||||
}
|
||||
|
||||
errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
loc,
|
||||
suggestions: null,
|
||||
errorType: call.invalidDeps.typeOfValue,
|
||||
invalidDepInfo: invalidDepInfo,
|
||||
loc: call.loc,
|
||||
setStateName:
|
||||
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,55 +639,12 @@ function validateNoRefAccessInRenderImpl(
|
||||
case 'StartMemoize':
|
||||
case 'FinishMemoize':
|
||||
break;
|
||||
case 'LoadGlobal': {
|
||||
if (instr.value.binding.name === 'undefined') {
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Primitive': {
|
||||
if (instr.value.value == null) {
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'UnaryExpression': {
|
||||
if (instr.value.operator === '!') {
|
||||
const value = env.get(instr.value.value.identifier.id);
|
||||
const refId =
|
||||
value?.kind === 'RefValue' && value.refId != null
|
||||
? value.refId
|
||||
: null;
|
||||
if (refId !== null) {
|
||||
/*
|
||||
* Record an error suggesting the `if (ref.current == null)` pattern,
|
||||
* but also record the lvalue as a guard so that we don't emit a second
|
||||
* error for the write to the ref
|
||||
*/
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
})
|
||||
.withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.value.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
})
|
||||
.withDetails({
|
||||
kind: 'hint',
|
||||
message:
|
||||
'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
validateNoRefValueAccess(errors, env, instr.value.value);
|
||||
break;
|
||||
}
|
||||
case 'BinaryExpression': {
|
||||
const left = env.get(instr.value.left.identifier.id);
|
||||
const right = env.get(instr.value.right.identifier.id);
|
||||
|
||||
@@ -11,23 +11,16 @@ import {
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
Environment,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
isUseRefType,
|
||||
isRefValueType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates against calling setState in the body of an effect (useEffect and friends),
|
||||
@@ -39,7 +32,6 @@ import {Iterable_some} from '../Utils/utils';
|
||||
*/
|
||||
export function validateNoSetStateInEffects(
|
||||
fn: HIRFunction,
|
||||
env: Environment,
|
||||
): Result<void, CompilerError> {
|
||||
const setStateFunctions: Map<IdentifierId, Place> = new Map();
|
||||
const errors = new CompilerError();
|
||||
@@ -80,7 +72,6 @@ export function validateNoSetStateInEffects(
|
||||
const callee = getSetStateCall(
|
||||
instr.value.loweredFunc.func,
|
||||
setStateFunctions,
|
||||
env,
|
||||
);
|
||||
if (callee !== null) {
|
||||
setStateFunctions.set(instr.lvalue.identifier.id, callee);
|
||||
@@ -138,42 +129,9 @@ export function validateNoSetStateInEffects(
|
||||
function getSetStateCall(
|
||||
fn: HIRFunction,
|
||||
setStateFunctions: Map<IdentifierId, Place>,
|
||||
env: Environment,
|
||||
): Place | null {
|
||||
const refDerivedValues: Set<IdentifierId> = new Set();
|
||||
|
||||
const isDerivedFromRef = (place: Place): boolean => {
|
||||
return (
|
||||
refDerivedValues.has(place.identifier.id) ||
|
||||
isUseRefType(place.identifier) ||
|
||||
isRefValueType(place.identifier)
|
||||
);
|
||||
};
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (env.config.enableAllowSetStateFromRefsInEffects) {
|
||||
const hasRefOperand = Iterable_some(
|
||||
eachInstructionValueOperand(instr.value),
|
||||
isDerivedFromRef,
|
||||
);
|
||||
|
||||
if (hasRefOperand) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
refDerivedValues.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'PropertyLoad' &&
|
||||
instr.value.property === 'current' &&
|
||||
(isUseRefType(instr.value.object.identifier) ||
|
||||
isRefValueType(instr.value.object.identifier))
|
||||
) {
|
||||
refDerivedValues.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
switch (instr.value.kind) {
|
||||
case 'LoadLocal': {
|
||||
if (setStateFunctions.has(instr.value.place.identifier.id)) {
|
||||
@@ -203,21 +161,6 @@ function getSetStateCall(
|
||||
isSetStateType(callee.identifier) ||
|
||||
setStateFunctions.has(callee.identifier.id)
|
||||
) {
|
||||
if (env.config.enableAllowSetStateFromRefsInEffects) {
|
||||
const arg = instr.value.args.at(0);
|
||||
if (
|
||||
arg !== undefined &&
|
||||
arg.kind === 'Identifier' &&
|
||||
refDerivedValues.has(arg.identifier.id)
|
||||
) {
|
||||
/**
|
||||
* The one special case where we allow setStates in effects is in the very specific
|
||||
* scenario where the value being set is derived from a ref. For example this may
|
||||
* be needed when initial layout measurements from refs need to be stored in state.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* TODO: once we support multiple locations per error, we should link to the
|
||||
* original Place in the case that setStateFunction.has(callee)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
if (r.current == undefined) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { useRef } from "react";
|
||||
|
||||
function C() {
|
||||
const r = useRef(null);
|
||||
if (r.current == undefined) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok)
|
||||
@@ -1,14 +0,0 @@
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
if (r.current == undefined) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -24,13 +24,15 @@ function BadExample() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
const current = !r.current;
|
||||
return <div>{current}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 4 errors:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | const current = !r.current;
|
||||
| ^^^^^^^^^ Cannot access ref value during render
|
||||
7 | return <div>{current}</div>;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | const current = !r.current;
|
||||
| ^^^^^^^^^^ Cannot access ref value during render
|
||||
7 | return <div>{current}</div>;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
5 | const r = useRef(null);
|
||||
6 | const current = !r.current;
|
||||
> 7 | return <div>{current}</div>;
|
||||
| ^^^^^^^ Cannot access ref value during render
|
||||
8 | }
|
||||
9 |
|
||||
10 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
5 | const r = useRef(null);
|
||||
6 | const current = !r.current;
|
||||
> 7 | return <div>{current}</div>;
|
||||
| ^^^^^^^ Cannot access ref value during render
|
||||
8 | }
|
||||
9 |
|
||||
10 | export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
const current = !r.current;
|
||||
return <div>{current}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
if (!r.current) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | if (!r.current) {
|
||||
| ^^^^^^^^^ Cannot access ref value during render
|
||||
7 | r.current = 1;
|
||||
8 | }
|
||||
9 | }
|
||||
|
||||
To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`
|
||||
```
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
if (!r.current) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component() {
|
||||
const [state, setState] = useCustomState(0);
|
||||
const aliased = setState;
|
||||
|
||||
setState(1);
|
||||
aliased(2);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function useCustomState(init) {
|
||||
return useState(init);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2
|
||||
4 | const aliased = setState;
|
||||
5 |
|
||||
> 6 | setState(1);
|
||||
| ^^^^^^^^ Found setState() in render
|
||||
7 | aliased(2);
|
||||
8 |
|
||||
9 | return state;
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-unconditional-set-state-hook-return-in-render.ts:7:2
|
||||
5 |
|
||||
6 | setState(1);
|
||||
> 7 | aliased(2);
|
||||
| ^^^^^^^ Found setState() in render
|
||||
8 |
|
||||
9 | return state;
|
||||
10 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component() {
|
||||
const [state, setState] = useCustomState(0);
|
||||
const aliased = setState;
|
||||
|
||||
setState(1);
|
||||
aliased(2);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function useCustomState(init) {
|
||||
return useState(init);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component({setX}) {
|
||||
const aliased = setX;
|
||||
|
||||
setX(1);
|
||||
aliased(2);
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-unconditional-set-state-prop-in-render.ts:5:2
|
||||
3 | const aliased = setX;
|
||||
4 |
|
||||
> 5 | setX(1);
|
||||
| ^^^^ Found setState() in render
|
||||
6 | aliased(2);
|
||||
7 |
|
||||
8 | return x;
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-unconditional-set-state-prop-in-render.ts:6:2
|
||||
4 |
|
||||
5 | setX(1);
|
||||
> 6 | aliased(2);
|
||||
| ^^^^^^^ Found setState() in render
|
||||
7 |
|
||||
8 | return x;
|
||||
9 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component({setX}) {
|
||||
const aliased = setX;
|
||||
|
||||
setX(1);
|
||||
aliased(2);
|
||||
|
||||
return x;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender
|
||||
|
||||
function useHook(parentRef) {
|
||||
// Some components accept a union of "callback" refs and ref objects, which
|
||||
// we can't currently represent
|
||||
const elementRef = useRef(null);
|
||||
const handler = instance => {
|
||||
elementRef.current = instance;
|
||||
if (parentRef != null) {
|
||||
if (typeof parentRef === 'function') {
|
||||
// This call infers the type of `parentRef` as a function...
|
||||
parentRef(instance);
|
||||
} else {
|
||||
// So this assignment fails since we don't know its a ref
|
||||
parentRef.current = instance;
|
||||
}
|
||||
}
|
||||
};
|
||||
return handler;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
|
||||
|
||||
error.todo-allow-assigning-to-inferred-ref-prop-in-callback.ts:15:8
|
||||
13 | } else {
|
||||
14 | // So this assignment fails since we don't know its a ref
|
||||
> 15 | parentRef.current = instance;
|
||||
| ^^^^^^^^^ `parentRef` cannot be modified
|
||||
16 | }
|
||||
17 | }
|
||||
18 | };
|
||||
```
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// @validateRefAccessDuringRender
|
||||
|
||||
function useHook(parentRef) {
|
||||
// Some components accept a union of "callback" refs and ref objects, which
|
||||
// we can't currently represent
|
||||
const elementRef = useRef(null);
|
||||
const handler = instance => {
|
||||
elementRef.current = instance;
|
||||
if (parentRef != null) {
|
||||
if (typeof parentRef === 'function') {
|
||||
// This call infers the type of `parentRef` as a function...
|
||||
parentRef(instance);
|
||||
} else {
|
||||
// So this assignment fails since we don't know its a ref
|
||||
parentRef.current = instance;
|
||||
}
|
||||
}
|
||||
};
|
||||
return handler;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
function Component() {
|
||||
const dispatch = useDispatch();
|
||||
// const [state, setState] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={event => {
|
||||
dispatch(...event.target);
|
||||
event.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useDispatch() {
|
||||
'use no memo';
|
||||
// skip compilation to make it easier to debug the above function
|
||||
return (...values) => {
|
||||
console.log(...values);
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const dispatch = useDispatch();
|
||||
let t0;
|
||||
if ($[0] !== dispatch) {
|
||||
t0 = (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(event) => {
|
||||
dispatch(...event.target);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
$[0] = dispatch;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
function useDispatch() {
|
||||
"use no memo";
|
||||
// skip compilation to make it easier to debug the above function
|
||||
return (...values) => {
|
||||
console.log(...values);
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input type="file"></div>
|
||||
@@ -1,30 +0,0 @@
|
||||
// @compilationMode:"infer"
|
||||
function Component() {
|
||||
const dispatch = useDispatch();
|
||||
// const [state, setState] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={event => {
|
||||
dispatch(...event.target);
|
||||
event.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useDispatch() {
|
||||
'use no memo';
|
||||
// skip compilation to make it easier to debug the above function
|
||||
return (...values) => {
|
||||
console.log(...values);
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { initialName } = t0;
|
||||
const [name, setName] = useState("");
|
||||
let t1;
|
||||
if ($[0] !== initialName) {
|
||||
t1 = () => {
|
||||
setName(initialName);
|
||||
};
|
||||
$[0] = initialName;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = (e) => setName(e.target.value);
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== name) {
|
||||
t4 = (
|
||||
<div>
|
||||
<input value={name} onChange={t3} />
|
||||
</div>
|
||||
);
|
||||
$[4] = name;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ initialName: "John" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="John"></div>
|
||||
@@ -0,0 +1,21 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { value, enabled } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== enabled || $[1] !== value) {
|
||||
t1 = () => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue("disabled");
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [value, enabled];
|
||||
$[0] = enabled;
|
||||
$[1] = value;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[4] = localValue;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test", enabled: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Value changed:', value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { value } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== value) {
|
||||
t1 = () => {
|
||||
console.log("Value changed:", value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
};
|
||||
t2 = [value];
|
||||
$[0] = value;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[3] = localValue;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
logs: ['Value changed:','test']
|
||||
@@ -0,0 +1,19 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Value changed:', value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({prefix}) {
|
||||
const [name, setName] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(prefix + name);
|
||||
}, [prefix, name]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: 'Hello, '}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.bug-derived-state-from-mixed-deps.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setDisplayName(prefix + name);
|
||||
| ^^^^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [prefix, name]);
|
||||
11 |
|
||||
12 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({prefix}) {
|
||||
const [name, setName] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(prefix + name);
|
||||
}, [prefix, name]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: 'Hello, '}],
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({user: {firstName, lastName}}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{user: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.invalid-derived-state-from-props-destructured.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
9 | }, [firstName, lastName]);
|
||||
10 |
|
||||
11 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.invalid-derived-state-from-props-in-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
9 | }, [firstName, lastName]);
|
||||
10 |
|
||||
11 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('John');
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.invalid-derived-state-from-state-in-effect.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
11 | }, [firstName, lastName]);
|
||||
12 |
|
||||
13 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('John');
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(7);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
|
||||
t0 = () => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
};
|
||||
t1 = [props.prefix, props.value, props.suffix];
|
||||
$[0] = props.prefix;
|
||||
$[1] = props.suffix;
|
||||
$[2] = props.value;
|
||||
$[3] = t0;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
t1 = $[4];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[5] !== displayValue) {
|
||||
t2 = <div>{displayValue}</div>;
|
||||
$[5] = displayValue;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prefix: "[", value: "test", suffix: "]" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>[test]</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const {height} = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
function Tooltip() {
|
||||
const $ = _c(2);
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
const { height } = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
};
|
||||
t1 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Cannot read properties of null (reading 'getBoundingClientRect')
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoSetStateInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const {height} = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useLayoutEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef({size: 5});
|
||||
const [computedSize, setComputedSize] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setComputedSize(ref.current.size * 10);
|
||||
}, []);
|
||||
|
||||
return computedSize;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = { size: 5 };
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const ref = useRef(t0);
|
||||
const [computedSize, setComputedSize] = useState(0);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
setComputedSize(ref.current.size * 10);
|
||||
};
|
||||
t2 = [];
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useLayoutEffect(t1, t2);
|
||||
return computedSize;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 50
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useLayoutEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef({size: 5});
|
||||
const [computedSize, setComputedSize] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setComputedSize(ref.current.size * 10);
|
||||
}, []);
|
||||
|
||||
return computedSize;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef([1, 2, 3, 4, 5]);
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const index = 2;
|
||||
setValue(ref.current[index]);
|
||||
}, []);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [1, 2, 3, 4, 5];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const ref = useRef(t0);
|
||||
const [value, setValue] = useState(0);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
setValue(ref.current[2]);
|
||||
};
|
||||
t2 = [];
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
return value;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 3
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef([1, 2, 3, 4, 5]);
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const index = 2;
|
||||
setValue(ref.current[index]);
|
||||
}, []);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
function getBoundingRect(ref) {
|
||||
if (ref.current) {
|
||||
return ref.current.getBoundingClientRect?.()?.width ?? 100;
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
|
||||
setWidth(getBoundingRect(ref));
|
||||
}, []);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const ref = useRef(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
const getBoundingRect = function getBoundingRect(ref_0) {
|
||||
if (ref_0.current) {
|
||||
return ref_0.current.getBoundingClientRect?.()?.width ?? 100;
|
||||
}
|
||||
return 100;
|
||||
};
|
||||
|
||||
setWidth(getBoundingRect(ref));
|
||||
};
|
||||
t1 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
return width;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 100
|
||||
@@ -1,25 +0,0 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
function getBoundingRect(ref) {
|
||||
if (ref.current) {
|
||||
return ref.current.getBoundingClientRect?.()?.width ?? 100;
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
|
||||
setWidth(getBoundingRect(ref));
|
||||
}, []);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useLayoutEffect} from 'react';
|
||||
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const {height} = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
|
||||
function Tooltip() {
|
||||
const $ = _c(2);
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
const { height } = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
};
|
||||
t1 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
useLayoutEffect(t0, t1);
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Cannot read properties of null (reading 'getBoundingClientRect')
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useLayoutEffect} from 'react';
|
||||
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const {height} = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
Activity,
|
||||
unstable_Activity as Activity,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useState,
|
||||
@@ -50,8 +50,7 @@ function Component() {
|
||||
<p>
|
||||
<img
|
||||
src="https://react.dev/_next/image?url=%2Fimages%2Fteam%2Fsebmarkbage.jpg&w=3840&q=75"
|
||||
width="400"
|
||||
height="248"
|
||||
width="300"
|
||||
/>
|
||||
</p>
|
||||
</ViewTransition>
|
||||
|
||||
@@ -56,10 +56,10 @@
|
||||
}
|
||||
|
||||
::view-transition-new(.enter-slide-right):only-child {
|
||||
animation: enter-slide-right ease-in 0.25s forwards;
|
||||
animation: enter-slide-right ease-in 0.25s;
|
||||
}
|
||||
::view-transition-old(.exit-slide-left):only-child {
|
||||
animation: exit-slide-left ease-in 0.25s forwards;
|
||||
animation: exit-slide-left ease-in 0.25s;
|
||||
}
|
||||
|
||||
:root:active-view-transition-type(navigation-back) {
|
||||
|
||||
10
packages/react-art/src/ReactFiberConfigART.js
vendored
10
packages/react-art/src/ReactFiberConfigART.js
vendored
@@ -609,15 +609,13 @@ export function preloadInstance(type, props) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function startSuspendingCommit() {
|
||||
return null;
|
||||
}
|
||||
export function startSuspendingCommit() {}
|
||||
|
||||
export function suspendInstance(state, instance, type, props) {}
|
||||
export function suspendInstance(instance, type, props) {}
|
||||
|
||||
export function suspendOnActiveViewTransition(state, container) {}
|
||||
export function suspendOnActiveViewTransition(container) {}
|
||||
|
||||
export function waitForCommitToBeReady(timeoutOffset) {
|
||||
export function waitForCommitToBeReady() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
24
packages/react-client/src/ReactFlightClient.js
vendored
24
packages/react-client/src/ReactFlightClient.js
vendored
@@ -3181,27 +3181,11 @@ function resolveErrorDev(
|
||||
'An error occurred in the Server Components render but no message was provided',
|
||||
),
|
||||
);
|
||||
|
||||
let ownerTask: null | ConsoleTask = null;
|
||||
if (errorInfo.owner != null) {
|
||||
const ownerRef = errorInfo.owner.slice(1);
|
||||
// TODO: This is not resilient to the owner loading later in an Error like a debug channel.
|
||||
// The whole error serialization should probably go through the regular model at least for DEV.
|
||||
const owner = getOutlinedModel(response, ownerRef, {}, '', createModel);
|
||||
if (owner !== null) {
|
||||
ownerTask = initializeFakeTask(response, owner);
|
||||
}
|
||||
}
|
||||
|
||||
if (ownerTask === null) {
|
||||
const rootTask = getRootTask(response, env);
|
||||
if (rootTask != null) {
|
||||
error = rootTask.run(callStack);
|
||||
} else {
|
||||
error = callStack();
|
||||
}
|
||||
const rootTask = getRootTask(response, env);
|
||||
if (rootTask != null) {
|
||||
error = rootTask.run(callStack);
|
||||
} else {
|
||||
error = ownerTask.run(callStack);
|
||||
error = callStack();
|
||||
}
|
||||
|
||||
(error: any).name = name;
|
||||
|
||||
@@ -108,7 +108,6 @@ module.exports = {
|
||||
{
|
||||
loader: 'workerize-loader',
|
||||
options: {
|
||||
// Workers would have to be exposed on a public path in order to outline them.
|
||||
inline: true,
|
||||
name: '[name]',
|
||||
},
|
||||
|
||||
49
packages/react-devtools-extensions/build.js
vendored
49
packages/react-devtools-extensions/build.js
vendored
@@ -6,7 +6,7 @@ const archiver = require('archiver');
|
||||
const {execSync} = require('child_process');
|
||||
const {readFileSync, writeFileSync, createWriteStream} = require('fs');
|
||||
const {copy, ensureDir, move, remove, pathExistsSync} = require('fs-extra');
|
||||
const {join, resolve, basename} = require('path');
|
||||
const {join, resolve} = require('path');
|
||||
const {getGitCommit} = require('./utils');
|
||||
|
||||
// These files are copied along with Webpack-bundled files
|
||||
@@ -66,31 +66,22 @@ const build = async (tempPath, manifestPath, envExtension = {}) => {
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
execSync(
|
||||
`${webpackPath} --config webpack.backend.js --output-path ${binPath}`,
|
||||
{
|
||||
cwd: __dirname,
|
||||
env: mergedEnv,
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
|
||||
// Make temp dir
|
||||
await ensureDir(zipPath);
|
||||
|
||||
const copiedManifestPath = join(zipPath, 'manifest.json');
|
||||
|
||||
let webpackStatsFilePath = null;
|
||||
// Copy unbuilt source files to zip dir to be packaged:
|
||||
await copy(binPath, join(zipPath, 'build'), {
|
||||
filter: filePath => {
|
||||
if (basename(filePath).startsWith('webpack-stats.')) {
|
||||
webpackStatsFilePath = filePath;
|
||||
// The ZIP is the actual extension and doesn't need this metadata.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (webpackStatsFilePath !== null) {
|
||||
await copy(
|
||||
webpackStatsFilePath,
|
||||
join(tempPath, basename(webpackStatsFilePath)),
|
||||
);
|
||||
webpackStatsFilePath = join(tempPath, basename(webpackStatsFilePath));
|
||||
}
|
||||
await copy(binPath, join(zipPath, 'build'));
|
||||
await copy(manifestPath, copiedManifestPath);
|
||||
await Promise.all(
|
||||
STATIC_FILES.map(file => copy(join(__dirname, file), join(zipPath, file))),
|
||||
@@ -129,11 +120,9 @@ const build = async (tempPath, manifestPath, envExtension = {}) => {
|
||||
archive.finalize();
|
||||
zipStream.on('close', () => resolvePromise());
|
||||
});
|
||||
|
||||
return webpackStatsFilePath;
|
||||
};
|
||||
|
||||
const postProcess = async (tempPath, destinationPath, webpackStatsFilePath) => {
|
||||
const postProcess = async (tempPath, destinationPath) => {
|
||||
const unpackedSourcePath = join(tempPath, 'zip');
|
||||
const packedSourcePath = join(tempPath, 'ReactDevTools.zip');
|
||||
const packedDestPath = join(destinationPath, 'ReactDevTools.zip');
|
||||
@@ -141,14 +130,6 @@ const postProcess = async (tempPath, destinationPath, webpackStatsFilePath) => {
|
||||
|
||||
await move(unpackedSourcePath, unpackedDestPath); // Copy built files to destination
|
||||
await move(packedSourcePath, packedDestPath); // Copy built files to destination
|
||||
if (webpackStatsFilePath !== null) {
|
||||
await move(
|
||||
webpackStatsFilePath,
|
||||
join(destinationPath, basename(webpackStatsFilePath)),
|
||||
);
|
||||
} else {
|
||||
console.log('No webpack-stats.json file was generated.');
|
||||
}
|
||||
await remove(tempPath); // Clean up temp directory and files
|
||||
};
|
||||
|
||||
@@ -177,14 +158,10 @@ const main = async buildId => {
|
||||
const tempPath = join(__dirname, 'build', buildId);
|
||||
await ensureLocalBuild();
|
||||
await preProcess(destinationPath, tempPath);
|
||||
const webpackStatsFilePath = await build(
|
||||
tempPath,
|
||||
manifestPath,
|
||||
envExtension,
|
||||
);
|
||||
await build(tempPath, manifestPath, envExtension);
|
||||
|
||||
const builtUnpackedPath = join(destinationPath, 'unpacked');
|
||||
await postProcess(tempPath, destinationPath, webpackStatsFilePath);
|
||||
await postProcess(tempPath, destinationPath);
|
||||
|
||||
return builtUnpackedPath;
|
||||
} catch (error) {
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
"webpack": "^5.82.1",
|
||||
"webpack-cli": "^5.1.1",
|
||||
"webpack-dev-server": "^4.15.0",
|
||||
"webpack-stats-plugin": "^1.1.3",
|
||||
"workerize-loader": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -82,7 +82,7 @@ const fetchFromPage = async (url, resolve, reject) => {
|
||||
debugLog('[main] fetchFromPage()', url);
|
||||
|
||||
function onPortMessage({payload, source}) {
|
||||
if (source === 'react-devtools-background' && payload?.url === url) {
|
||||
if (source === 'react-devtools-background') {
|
||||
switch (payload?.type) {
|
||||
case 'fetch-file-with-cache-complete':
|
||||
chrome.runtime.onMessage.removeListener(onPortMessage);
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
normalizeUrlIfValid,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {checkConditions} from 'react-devtools-shared/src/devtools/views/Editor/utils';
|
||||
import * as parseHookNames from 'react-devtools-shared/src/hooks/parseHookNames';
|
||||
|
||||
import {
|
||||
setBrowserSelectionFromReact,
|
||||
@@ -41,12 +40,6 @@ import getProfilingFlags from './getProfilingFlags';
|
||||
import debounce from './debounce';
|
||||
import './requestAnimationFramePolyfill';
|
||||
|
||||
const resolvedParseHookNames = Promise.resolve(parseHookNames);
|
||||
// DevTools assumes this is a dynamically imported module. Since we outline
|
||||
// workers in this bundle, we can sync require the module since it's just a thin
|
||||
// wrapper around calling the worker.
|
||||
const hookNamesModuleLoaderFunction = () => resolvedParseHookNames;
|
||||
|
||||
function createBridge() {
|
||||
bridge = new Bridge({
|
||||
listen(fn) {
|
||||
@@ -195,6 +188,12 @@ function createBridgeAndStore() {
|
||||
);
|
||||
};
|
||||
|
||||
// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
|
||||
const hookNamesModuleLoaderFunction = () =>
|
||||
import(
|
||||
/* webpackChunkName: 'parseHookNames' */ 'react-devtools-shared/src/hooks/parseHookNames'
|
||||
);
|
||||
|
||||
root = createRoot(document.createElement('div'));
|
||||
|
||||
render = (overrideTab = mostRecentOverrideTab) => {
|
||||
|
||||
118
packages/react-devtools-extensions/webpack.backend.js
Normal file
118
packages/react-devtools-extensions/webpack.backend.js
Normal file
@@ -0,0 +1,118 @@
|
||||
'use strict';
|
||||
|
||||
const {resolve, isAbsolute, relative} = require('path');
|
||||
const Webpack = require('webpack');
|
||||
|
||||
const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
|
||||
const SourceMapIgnoreListPlugin = require('react-devtools-shared/SourceMapIgnoreListPlugin');
|
||||
|
||||
const {GITHUB_URL, getVersionString} = require('./utils');
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
console.error('NODE_ENV not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const builtModulesDir = resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'build',
|
||||
'oss-experimental',
|
||||
);
|
||||
|
||||
const __DEV__ = NODE_ENV === 'development';
|
||||
|
||||
const DEVTOOLS_VERSION = getVersionString(process.env.DEVTOOLS_VERSION);
|
||||
|
||||
const IS_CHROME = process.env.IS_CHROME === 'true';
|
||||
const IS_FIREFOX = process.env.IS_FIREFOX === 'true';
|
||||
const IS_EDGE = process.env.IS_EDGE === 'true';
|
||||
|
||||
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
|
||||
|
||||
module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: false,
|
||||
entry: {
|
||||
backend: './src/backend.js',
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/build',
|
||||
filename: 'react_devtools_backend_compact.js',
|
||||
},
|
||||
node: {
|
||||
global: false,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
react: resolve(builtModulesDir, 'react'),
|
||||
'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
|
||||
'react-devtools-feature-flags': resolveFeatureFlags(featureFlagTarget),
|
||||
'react-dom': resolve(builtModulesDir, 'react-dom'),
|
||||
'react-is': resolve(builtModulesDir, 'react-is'),
|
||||
scheduler: resolve(builtModulesDir, 'scheduler'),
|
||||
},
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
plugins: [
|
||||
new Webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
}),
|
||||
new Webpack.DefinePlugin({
|
||||
__DEV__: true,
|
||||
__PROFILE__: false,
|
||||
__DEV____DEV__: true,
|
||||
// By importing `shared/` we may import ReactFeatureFlags
|
||||
__EXPERIMENTAL__: true,
|
||||
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
|
||||
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
|
||||
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
|
||||
'process.env.IS_CHROME': IS_CHROME,
|
||||
'process.env.IS_FIREFOX': IS_FIREFOX,
|
||||
'process.env.IS_EDGE': IS_EDGE,
|
||||
__IS_CHROME__: IS_CHROME,
|
||||
__IS_FIREFOX__: IS_FIREFOX,
|
||||
__IS_EDGE__: IS_EDGE,
|
||||
__IS_NATIVE__: false,
|
||||
__IS_INTERNAL_MCP_BUILD__: false,
|
||||
}),
|
||||
new Webpack.SourceMapDevToolPlugin({
|
||||
filename: '[file].map',
|
||||
noSources: !__DEV__,
|
||||
// https://github.com/webpack/webpack/issues/3603#issuecomment-1743147144
|
||||
moduleFilenameTemplate(info) {
|
||||
const {absoluteResourcePath, namespace, resourcePath} = info;
|
||||
|
||||
if (isAbsolute(absoluteResourcePath)) {
|
||||
return relative(__dirname + '/build', absoluteResourcePath);
|
||||
}
|
||||
|
||||
// Mimic Webpack's default behavior:
|
||||
return `webpack://${namespace}/${resourcePath}`;
|
||||
},
|
||||
}),
|
||||
new SourceMapIgnoreListPlugin({
|
||||
shouldIgnoreSource: () => !__DEV__,
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
configFile: resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'react-devtools-shared',
|
||||
'babel.config.js',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -6,7 +6,6 @@ const TerserPlugin = require('terser-webpack-plugin');
|
||||
const {GITHUB_URL, getVersionString} = require('./utils');
|
||||
const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
|
||||
const SourceMapIgnoreListPlugin = require('react-devtools-shared/SourceMapIgnoreListPlugin');
|
||||
const {StatsWriterPlugin} = require('webpack-stats-plugin');
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
@@ -38,21 +37,6 @@ const IS_INTERNAL_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true';
|
||||
|
||||
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
|
||||
|
||||
let statsFileName = `webpack-stats.${featureFlagTarget}.${__DEV__ ? 'development' : 'production'}`;
|
||||
if (IS_CHROME) {
|
||||
statsFileName += `.chrome`;
|
||||
}
|
||||
if (IS_FIREFOX) {
|
||||
statsFileName += `.firefox`;
|
||||
}
|
||||
if (IS_EDGE) {
|
||||
statsFileName += `.edge`;
|
||||
}
|
||||
if (IS_INTERNAL_MCP_BUILD) {
|
||||
statsFileName += `.mcp`;
|
||||
}
|
||||
statsFileName += '.json';
|
||||
|
||||
const babelOptions = {
|
||||
configFile: resolve(
|
||||
__dirname,
|
||||
@@ -66,7 +50,6 @@ module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: false,
|
||||
entry: {
|
||||
backend: './src/backend.js',
|
||||
background: './src/background/index.js',
|
||||
backendManager: './src/contentScripts/backendManager.js',
|
||||
fileFetcher: './src/contentScripts/fileFetcher.js',
|
||||
@@ -80,14 +63,7 @@ module.exports = {
|
||||
output: {
|
||||
path: __dirname + '/build',
|
||||
publicPath: '/build/',
|
||||
filename: chunkData => {
|
||||
switch (chunkData.chunk.name) {
|
||||
case 'backend':
|
||||
return 'react_devtools_backend_compact.js';
|
||||
default:
|
||||
return '[name].js';
|
||||
}
|
||||
},
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].chunk.js',
|
||||
},
|
||||
node: {
|
||||
@@ -127,6 +103,7 @@ module.exports = {
|
||||
plugins: [
|
||||
new Webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
new Webpack.DefinePlugin({
|
||||
__DEV__,
|
||||
@@ -149,7 +126,7 @@ module.exports = {
|
||||
}),
|
||||
new Webpack.SourceMapDevToolPlugin({
|
||||
filename: '[file].map',
|
||||
include: ['installHook.js', 'react_devtools_backend_compact.js'],
|
||||
include: 'installHook.js',
|
||||
noSources: !__DEV__,
|
||||
// https://github.com/webpack/webpack/issues/3603#issuecomment-1743147144
|
||||
moduleFilenameTemplate(info) {
|
||||
@@ -171,7 +148,6 @@ module.exports = {
|
||||
}
|
||||
|
||||
const contentScriptNamesToIgnoreList = [
|
||||
'react_devtools_backend_compact',
|
||||
// This is where we override console
|
||||
'installHook',
|
||||
];
|
||||
@@ -237,10 +213,6 @@ module.exports = {
|
||||
);
|
||||
},
|
||||
},
|
||||
new StatsWriterPlugin({
|
||||
stats: 'verbose',
|
||||
filename: statsFileName,
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
defaultRules: [
|
||||
@@ -261,7 +233,7 @@ module.exports = {
|
||||
{
|
||||
loader: 'workerize-loader',
|
||||
options: {
|
||||
inline: false,
|
||||
inline: true,
|
||||
name: '[name]',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -74,6 +74,7 @@ module.exports = {
|
||||
new MiniCssExtractPlugin(),
|
||||
new Webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
new Webpack.DefinePlugin({
|
||||
__DEV__,
|
||||
@@ -101,7 +102,6 @@ module.exports = {
|
||||
{
|
||||
loader: 'workerize-loader',
|
||||
options: {
|
||||
// Workers would have to be exposed on a public path in order to outline them.
|
||||
inline: true,
|
||||
name: '[name]',
|
||||
},
|
||||
|
||||
@@ -93,9 +93,7 @@ test.describe('Components', () => {
|
||||
|
||||
const name = isEditable.name
|
||||
? existingNameElements[0].value
|
||||
: existingNameElements[0].innerText
|
||||
// remove trailing colon
|
||||
.slice(0, -1);
|
||||
: existingNameElements[0].innerText;
|
||||
const value = isEditable.value
|
||||
? existingValueElements[0].value
|
||||
: existingValueElements[0].innerText;
|
||||
|
||||
@@ -65,6 +65,7 @@ module.exports = {
|
||||
plugins: [
|
||||
new Webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
new Webpack.DefinePlugin({
|
||||
__DEV__,
|
||||
@@ -93,7 +94,6 @@ module.exports = {
|
||||
{
|
||||
loader: 'workerize-loader',
|
||||
options: {
|
||||
// Workers would have to be exposed on a public path in order to outline them.
|
||||
inline: true,
|
||||
name: '[name]',
|
||||
},
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
"react-dom-15": "npm:react-dom@^15"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/parser": "^7.12.5",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@reach/menu-button": "^0.16.1",
|
||||
"@reach/tooltip": "^0.16.0",
|
||||
"clipboard-js": "^0.3.6",
|
||||
|
||||
@@ -12,8 +12,8 @@ export function test(maybeStore) {
|
||||
}
|
||||
|
||||
// print() is part of Jest's serializer API
|
||||
export function print(store, serialize, indent, includeSuspense = true) {
|
||||
return printStore(store, false, null, includeSuspense);
|
||||
export function print(store, serialize, indent) {
|
||||
return printStore(store);
|
||||
}
|
||||
|
||||
// Used for Jest snapshot testing.
|
||||
|
||||
@@ -724,69 +724,34 @@ describe('ProfilingCache', () => {
|
||||
const rootID = store.roots[0];
|
||||
const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
|
||||
expect(commitData).toHaveLength(2);
|
||||
|
||||
const isLegacySuspense = React.version.startsWith('17');
|
||||
if (isLegacySuspense) {
|
||||
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 15,
|
||||
2 => 15,
|
||||
3 => 5,
|
||||
4 => 3,
|
||||
5 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[0].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 0,
|
||||
2 => 10,
|
||||
3 => 3,
|
||||
4 => 3,
|
||||
5 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
6 => 3,
|
||||
3 => 3,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
6 => 3,
|
||||
3 => 0,
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 15,
|
||||
2 => 15,
|
||||
3 => 5,
|
||||
4 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[0].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 0,
|
||||
2 => 10,
|
||||
3 => 3,
|
||||
4 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
5 => 3,
|
||||
3 => 3,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
5 => 3,
|
||||
3 => 0,
|
||||
}
|
||||
`);
|
||||
}
|
||||
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 15,
|
||||
2 => 15,
|
||||
3 => 5,
|
||||
4 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[0].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 0,
|
||||
2 => 10,
|
||||
3 => 3,
|
||||
4 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
5 => 3,
|
||||
3 => 3,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
5 => 3,
|
||||
3 => 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 16.9
|
||||
@@ -901,7 +866,6 @@ describe('ProfilingCache', () => {
|
||||
"hocDisplayNames": null,
|
||||
"id": 1,
|
||||
"key": null,
|
||||
"stack": null,
|
||||
"type": 11,
|
||||
},
|
||||
],
|
||||
@@ -944,7 +908,6 @@ describe('ProfilingCache', () => {
|
||||
"hocDisplayNames": null,
|
||||
"id": 1,
|
||||
"key": null,
|
||||
"stack": null,
|
||||
"type": 11,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -15,12 +15,10 @@ import {
|
||||
} from './utils';
|
||||
|
||||
describe('commit tree', () => {
|
||||
let React = require('react');
|
||||
let React;
|
||||
let Scheduler;
|
||||
let store: Store;
|
||||
let utils;
|
||||
const isLegacySuspense =
|
||||
React.version.startsWith('16') || React.version.startsWith('17');
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
@@ -186,32 +184,17 @@ describe('commit tree', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
await Promise.resolve();
|
||||
if (isLegacySuspense) {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<Lazy>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
} else {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
}
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
`);
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<LazyInnerComponent>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
utils.act(() => legacyRender(<App renderChildren={false} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
@@ -231,13 +214,7 @@ describe('commit tree', () => {
|
||||
);
|
||||
}
|
||||
|
||||
expect(commitTrees[0].nodes.size).toBe(
|
||||
isLegacySuspense
|
||||
? // <Root> + <App> + <Suspense> + <Lazy>
|
||||
4
|
||||
: // <Root> + <App> + <Suspense>
|
||||
3,
|
||||
);
|
||||
expect(commitTrees[0].nodes.size).toBe(3); // <Root> + <App> + <Suspense>
|
||||
expect(commitTrees[1].nodes.size).toBe(4); // <Root> + <App> + <Suspense> + <LazyInnerComponent>
|
||||
expect(commitTrees[2].nodes.size).toBe(2); // <Root> + <App>
|
||||
});
|
||||
@@ -291,24 +268,11 @@ describe('commit tree', () => {
|
||||
it('should support Lazy components that are unmounted before resolving (legacy render)', async () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
if (isLegacySuspense) {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<Lazy>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
} else {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
}
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
`);
|
||||
utils.act(() => legacyRender(<App renderChildren={false} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
@@ -327,13 +291,7 @@ describe('commit tree', () => {
|
||||
);
|
||||
}
|
||||
|
||||
expect(commitTrees[0].nodes.size).toBe(
|
||||
isLegacySuspense
|
||||
? // <Root> + <App> + <Suspense> + <Lazy>
|
||||
4
|
||||
: // <Root> + <App> + <Suspense>
|
||||
3,
|
||||
);
|
||||
expect(commitTrees[0].nodes.size).toBe(3); // <Root> + <App> + <Suspense>
|
||||
expect(commitTrees[1].nodes.size).toBe(2); // <Root> + <App>
|
||||
});
|
||||
|
||||
|
||||
@@ -24,35 +24,6 @@ describe('Store', () => {
|
||||
let store;
|
||||
let withErrorsOrWarningsIgnored;
|
||||
|
||||
function readValue(promise) {
|
||||
if (typeof React.use === 'function') {
|
||||
return React.use(promise);
|
||||
}
|
||||
|
||||
// Support for React < 19.0
|
||||
switch (promise.status) {
|
||||
case 'fulfilled':
|
||||
return promise.value;
|
||||
case 'rejected':
|
||||
throw promise.reason;
|
||||
case 'pending':
|
||||
throw promise;
|
||||
default:
|
||||
promise.status = 'pending';
|
||||
promise.then(
|
||||
value => {
|
||||
promise.status = 'fulfilled';
|
||||
promise.value = value;
|
||||
},
|
||||
reason => {
|
||||
promise.status = 'rejected';
|
||||
promise.reason = reason;
|
||||
},
|
||||
);
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
// JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
|
||||
Element.prototype.getClientRects = function (this: Element) {
|
||||
@@ -136,7 +107,11 @@ describe('Store', () => {
|
||||
let Dynamic = null;
|
||||
const Owner = () => {
|
||||
Dynamic = <Child />;
|
||||
readValue(promise);
|
||||
if (React.use) {
|
||||
React.use(promise);
|
||||
} else {
|
||||
throw promise;
|
||||
}
|
||||
};
|
||||
const Parent = () => {
|
||||
return Dynamic;
|
||||
@@ -487,9 +462,12 @@ describe('Store', () => {
|
||||
// @reactVersion >= 18.0
|
||||
it('should display Suspense nodes properly in various states', async () => {
|
||||
const Loading = () => <div>Loading...</div>;
|
||||
const never = new Promise(() => {});
|
||||
const SuspendingComponent = () => {
|
||||
readValue(never);
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
};
|
||||
const Component = () => {
|
||||
return <div>Hello</div>;
|
||||
@@ -536,9 +514,12 @@ describe('Store', () => {
|
||||
it('should support nested Suspense nodes', async () => {
|
||||
const Component = () => null;
|
||||
const Loading = () => <div>Loading...</div>;
|
||||
const never = new Promise(() => {});
|
||||
const Never = () => {
|
||||
readValue(never);
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const Wrapper = ({
|
||||
@@ -1038,9 +1019,12 @@ describe('Store', () => {
|
||||
|
||||
it('should display a partially rendered SuspenseList', async () => {
|
||||
const Loading = () => <div>Loading...</div>;
|
||||
const never = new Promise(() => {});
|
||||
const SuspendingComponent = () => {
|
||||
readValue(never);
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
};
|
||||
const Component = () => {
|
||||
return <div>Hello</div>;
|
||||
@@ -1395,9 +1379,12 @@ describe('Store', () => {
|
||||
// @reactVersion >= 18.0
|
||||
it('should display Suspense nodes properly in various states', async () => {
|
||||
const Loading = () => <div>Loading...</div>;
|
||||
const never = new Promise(() => {});
|
||||
const SuspendingComponent = () => {
|
||||
readValue(never);
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
};
|
||||
const Component = () => {
|
||||
return <div>Hello</div>;
|
||||
@@ -2094,8 +2081,6 @@ describe('Store', () => {
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
|
||||
// Render again to unmount it before it finishes loading
|
||||
@@ -2504,7 +2489,7 @@ describe('Store', () => {
|
||||
withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
||||
await act(() => render(<React.Fragment />));
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
expect(store).toMatchInlineSnapshot(`[root]`);
|
||||
expect(store.componentWithErrorCount).toBe(0);
|
||||
expect(store.componentWithWarningCount).toBe(0);
|
||||
});
|
||||
@@ -2841,7 +2826,7 @@ describe('Store', () => {
|
||||
|
||||
function Component({children, promise}) {
|
||||
if (promise) {
|
||||
readValue(promise);
|
||||
React.use(promise);
|
||||
}
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
@@ -2916,7 +2901,7 @@ describe('Store', () => {
|
||||
|
||||
function Component({children, promise}) {
|
||||
if (promise) {
|
||||
readValue(promise);
|
||||
React.use(promise);
|
||||
}
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
@@ -3094,17 +3079,10 @@ describe('Store', () => {
|
||||
<Suspense name="head-fallback" rects={[{x:1,y:2,width:10,height:1}]}>
|
||||
<Suspense name="main" rects={[{x:1,y:2,width:4,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => render(null));
|
||||
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
});
|
||||
|
||||
it('should handle an empty root', async () => {
|
||||
await actAsync(() => render(null));
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
|
||||
await actAsync(() => render(<span />));
|
||||
expect(store).toMatchInlineSnapshot(`[root]`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('Store component filters', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 16.6
|
||||
// @reactVersion >= 16.0
|
||||
it('should filter Suspense', async () => {
|
||||
const Suspense = React.Suspense;
|
||||
await actAsync(async () =>
|
||||
@@ -199,7 +199,7 @@ describe('Store component filters', () => {
|
||||
});
|
||||
|
||||
it('should filter Activity', async () => {
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
const Activity = React.unstable_Activity;
|
||||
|
||||
if (Activity != null) {
|
||||
await actAsync(async () =>
|
||||
|
||||
@@ -16,35 +16,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
let store;
|
||||
let print;
|
||||
|
||||
function readValue(promise) {
|
||||
if (typeof React.use === 'function') {
|
||||
return React.use(promise);
|
||||
}
|
||||
|
||||
// Support for React < 19.0
|
||||
switch (promise.status) {
|
||||
case 'fulfilled':
|
||||
return promise.value;
|
||||
case 'rejected':
|
||||
throw promise.reason;
|
||||
case 'pending':
|
||||
throw promise;
|
||||
default:
|
||||
promise.status = 'pending';
|
||||
promise.then(
|
||||
value => {
|
||||
promise.status = 'fulfilled';
|
||||
promise.value = value;
|
||||
},
|
||||
reason => {
|
||||
promise.status = 'rejected';
|
||||
promise.reason = reason;
|
||||
},
|
||||
);
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
@@ -444,116 +415,118 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
a,
|
||||
];
|
||||
|
||||
// Excluding Suspense tree here due to different measurement semantics for fallbacks
|
||||
const stepsSnapshot = [
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<B key="b">
|
||||
<A key="a">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
<Suspense>
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<B key="b">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
`
|
||||
"[root]
|
||||
[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>"
|
||||
<Y>
|
||||
`,
|
||||
];
|
||||
|
||||
const never = new Promise(() => {});
|
||||
const Never = () => {
|
||||
readValue(never);
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const Root = ({children}) => {
|
||||
@@ -576,10 +549,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// We snapshot each step once so it doesn't regress.
|
||||
expect(print(store, undefined, undefined, false)).toMatchInlineSnapshot(
|
||||
stepsSnapshot[i],
|
||||
);
|
||||
snapshots.push(print(store, undefined, undefined, false));
|
||||
expect(store).toMatchInlineSnapshot(stepsSnapshot[i]);
|
||||
snapshots.push(print(store));
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
}
|
||||
@@ -601,7 +572,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
}
|
||||
@@ -621,7 +592,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -633,7 +604,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -644,7 +615,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -670,7 +641,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -686,7 +657,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -701,7 +672,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -723,7 +694,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -739,7 +710,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -750,7 +721,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -776,7 +747,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -788,7 +759,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -803,7 +774,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -832,7 +803,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
const suspenseID = store.getElementIDAtIndex(2);
|
||||
|
||||
// Force fallback.
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
act(() => {
|
||||
bridge.send('overrideSuspense', {
|
||||
id: suspenseID,
|
||||
@@ -840,7 +811,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
forceFallback: true,
|
||||
});
|
||||
});
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
|
||||
// Stop forcing fallback.
|
||||
act(() => {
|
||||
@@ -850,7 +821,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
forceFallback: false,
|
||||
});
|
||||
});
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
|
||||
// Trigger actual fallback.
|
||||
act(() =>
|
||||
@@ -866,7 +837,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
|
||||
// Force fallback while we're in fallback mode.
|
||||
act(() => {
|
||||
@@ -877,7 +848,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
});
|
||||
});
|
||||
// Keep seeing fallback content.
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
|
||||
// Switch to primary mode.
|
||||
act(() =>
|
||||
@@ -890,7 +861,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Fallback is still forced though.
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
|
||||
// Stop forcing fallback. This reverts to primary content.
|
||||
act(() => {
|
||||
@@ -901,7 +872,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
});
|
||||
});
|
||||
// Now we see primary content.
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
@@ -950,8 +921,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -962,8 +931,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -976,8 +943,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<C key="c">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -990,8 +955,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1003,8 +966,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1016,8 +977,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1029,8 +988,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1042,8 +999,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<B key="b">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1054,8 +1009,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1065,8 +1018,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <MaybeSuspend>
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1077,8 +1028,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<B key="b">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1089,8 +1038,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1102,8 +1049,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1112,8 +1057,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1124,8 +1067,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1136,8 +1077,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<B key="b">
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1147,8 +1086,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1158,8 +1095,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1169,8 +1104,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1180,8 +1113,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1190,8 +1121,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1199,8 +1128,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<X>
|
||||
<Suspense>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1209,8 +1136,6 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<B key="b">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1219,14 +1144,15 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
];
|
||||
|
||||
const never = new Promise(() => {});
|
||||
const Never = () => {
|
||||
readValue(never);
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const MaybeSuspend = ({children, suspend}) => {
|
||||
@@ -1298,7 +1224,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
);
|
||||
// We snapshot each step once so it doesn't regress.
|
||||
expect(store).toMatchInlineSnapshot(stepsSnapshotTwo[i]);
|
||||
fallbackSnapshots.push(print(store, undefined, undefined, false));
|
||||
fallbackSnapshots.push(print(store));
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
}
|
||||
@@ -1376,9 +1302,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[i],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -1397,9 +1321,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -1417,9 +1339,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[i],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -1457,9 +1377,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -1496,9 +1414,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[i],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -1525,9 +1441,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[i],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -1566,9 +1480,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
forceFallback: true,
|
||||
});
|
||||
});
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
|
||||
// Stop forcing fallback.
|
||||
act(() => {
|
||||
@@ -1592,9 +1504,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
|
||||
// Force fallback while we're in fallback mode.
|
||||
act(() => {
|
||||
@@ -1605,9 +1515,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
});
|
||||
});
|
||||
// Keep seeing fallback content.
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
|
||||
// Switch to primary mode.
|
||||
act(() =>
|
||||
@@ -1622,9 +1530,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Fallback is still forced though.
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
|
||||
// Stop forcing fallback. This reverts to primary content.
|
||||
act(() => {
|
||||
|
||||
@@ -88,7 +88,6 @@ import {
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
SUSPENSE_TREE_OPERATION_RESIZE,
|
||||
SUSPENSE_TREE_OPERATION_SUSPENDERS,
|
||||
UNKNOWN_SUSPENDERS_NONE,
|
||||
UNKNOWN_SUSPENDERS_REASON_PRODUCTION,
|
||||
UNKNOWN_SUSPENDERS_REASON_OLD_VERSION,
|
||||
@@ -1516,7 +1515,7 @@ export function attach(
|
||||
currentRoot = rootInstance;
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
flushPendingEvents();
|
||||
flushPendingEvents(root);
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
@@ -1541,7 +1540,7 @@ export function attach(
|
||||
currentRoot = newRoot;
|
||||
setRootPseudoKey(currentRoot.id, root.current);
|
||||
mountFiberRecursively(root.current, false);
|
||||
flushPendingEvents();
|
||||
flushPendingEvents(root);
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
@@ -2017,7 +2016,6 @@ export function attach(
|
||||
const pendingOperations: OperationsArray = [];
|
||||
const pendingRealUnmountedIDs: Array<FiberInstance['id']> = [];
|
||||
const pendingRealUnmountedSuspenseIDs: Array<FiberInstance['id']> = [];
|
||||
const pendingSuspenderChanges: Set<FiberInstance['id']> = new Set();
|
||||
let pendingOperationsQueue: Array<OperationsArray> | null = [];
|
||||
const pendingStringTable: Map<string, StringTableEntry> = new Map();
|
||||
let pendingStringTableLength: number = 0;
|
||||
@@ -2049,7 +2047,6 @@ export function attach(
|
||||
pendingOperations.length === 0 &&
|
||||
pendingRealUnmountedIDs.length === 0 &&
|
||||
pendingRealUnmountedSuspenseIDs.length === 0 &&
|
||||
pendingSuspenderChanges.size === 0 &&
|
||||
pendingUnmountedRootID === null
|
||||
);
|
||||
}
|
||||
@@ -2099,7 +2096,7 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function flushPendingEvents(): void {
|
||||
function flushPendingEvents(root: Object): void {
|
||||
if (shouldBailoutWithPendingOperations()) {
|
||||
// If we aren't profiling, we can just bail out here.
|
||||
// No use sending an empty update over the bridge.
|
||||
@@ -2116,7 +2113,6 @@ export function attach(
|
||||
pendingRealUnmountedIDs.length +
|
||||
(pendingUnmountedRootID === null ? 0 : 1);
|
||||
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
|
||||
const numSuspenderChanges = pendingSuspenderChanges.size;
|
||||
|
||||
const operations = new Array<number>(
|
||||
// Identify which renderer this update is coming from.
|
||||
@@ -2132,10 +2128,7 @@ export function attach(
|
||||
// [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
|
||||
(numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
|
||||
// Regular operations
|
||||
pendingOperations.length +
|
||||
// All suspender changes are batched in a single message.
|
||||
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders]]
|
||||
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 2 : 0),
|
||||
pendingOperations.length,
|
||||
);
|
||||
|
||||
// Identify which renderer this update is coming from.
|
||||
@@ -2198,31 +2191,12 @@ export function attach(
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in pending operations.
|
||||
// Fill in the rest of the operations.
|
||||
for (let j = 0; j < pendingOperations.length; j++) {
|
||||
operations[i + j] = pendingOperations[j];
|
||||
}
|
||||
i += pendingOperations.length;
|
||||
|
||||
// Suspender changes might affect newly mounted nodes that we already recorded
|
||||
// in pending operations.
|
||||
if (numSuspenderChanges > 0) {
|
||||
operations[i++] = SUSPENSE_TREE_OPERATION_SUSPENDERS;
|
||||
operations[i++] = numSuspenderChanges;
|
||||
pendingSuspenderChanges.forEach(fiberIdWithChanges => {
|
||||
const suspense = idToSuspenseNodeMap.get(fiberIdWithChanges);
|
||||
if (suspense === undefined) {
|
||||
// Probably forgot to cleanup pendingSuspenderChanges when this node was removed.
|
||||
throw new Error(
|
||||
`Could not send suspender changes for "${fiberIdWithChanges}" since the Fiber no longer exists.`,
|
||||
);
|
||||
}
|
||||
operations[i++] = fiberIdWithChanges;
|
||||
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Let the frontend know about tree operations.
|
||||
flushOrQueueOperations(operations);
|
||||
|
||||
@@ -2230,7 +2204,6 @@ export function attach(
|
||||
pendingOperations.length = 0;
|
||||
pendingRealUnmountedIDs.length = 0;
|
||||
pendingRealUnmountedSuspenseIDs.length = 0;
|
||||
pendingSuspenderChanges.clear();
|
||||
pendingUnmountedRootID = null;
|
||||
pendingStringTable.clear();
|
||||
pendingStringTableLength = 0;
|
||||
@@ -2715,19 +2688,6 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function recordSuspenseSuspenders(suspenseNode: SuspenseNode): void {
|
||||
if (__DEBUG__) {
|
||||
console.log('recordSuspenseSuspenders()', suspenseNode);
|
||||
}
|
||||
const fiberInstance = suspenseNode.instance;
|
||||
if (fiberInstance.kind !== FIBER_INSTANCE) {
|
||||
// TODO: Suspender updates of filtered Suspense nodes are currently dropped.
|
||||
return;
|
||||
}
|
||||
|
||||
pendingSuspenderChanges.add(fiberInstance.id);
|
||||
}
|
||||
|
||||
function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void {
|
||||
if (__DEBUG__) {
|
||||
console.log(
|
||||
@@ -2749,7 +2709,6 @@ export function attach(
|
||||
// and later arrange them in the correct order.
|
||||
pendingRealUnmountedSuspenseIDs.push(id);
|
||||
|
||||
pendingSuspenderChanges.delete(id);
|
||||
idToSuspenseNodeMap.delete(id);
|
||||
}
|
||||
|
||||
@@ -2820,7 +2779,6 @@ export function attach(
|
||||
) {
|
||||
// This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
|
||||
parentSuspenseNode.hasUniqueSuspenders = true;
|
||||
recordSuspenseSuspenders(parentSuspenseNode);
|
||||
}
|
||||
}
|
||||
// We have observed at least one known reason this might have been suspended.
|
||||
@@ -2862,9 +2820,6 @@ export function attach(
|
||||
// We have found a child boundary that depended on the unblocked I/O.
|
||||
// It can now be marked as having unique suspenders. We can skip its children
|
||||
// since they'll still be blocked by this one.
|
||||
if (!node.hasUniqueSuspenders) {
|
||||
recordSuspenseSuspenders(node);
|
||||
}
|
||||
node.hasUniqueSuspenders = true;
|
||||
node.hasUnknownSuspenders = false;
|
||||
} else if (node.firstChild !== null) {
|
||||
@@ -2902,32 +2857,16 @@ export function attach(
|
||||
// Let's remove it from the parent SuspenseNode.
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
const suspendedBySet = parentSuspenseNode.suspendedBy.get(ioInfo);
|
||||
|
||||
if (
|
||||
suspendedBySet === undefined ||
|
||||
!suspendedBySet.delete(instance)
|
||||
) {
|
||||
// A boundary can await the same IO multiple times.
|
||||
// We still want to error if we're trying to remove IO that isn't present on
|
||||
// this boundary so we need to check if we've already removed it.
|
||||
// We're assuming previousSuspendedBy is a small array so this should be faster
|
||||
// than allocating and maintaining a Set.
|
||||
let alreadyRemovedIO = false;
|
||||
for (let j = 0; j < i; j++) {
|
||||
const removedIOInfo = previousSuspendedBy[j].awaited;
|
||||
if (removedIOInfo === ioInfo) {
|
||||
alreadyRemovedIO = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alreadyRemovedIO) {
|
||||
throw new Error(
|
||||
'We are cleaning up async info that was not on the parent Suspense boundary. ' +
|
||||
'This is a bug in React.',
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
'We are cleaning up async info that was not on the parent Suspense boundary. ' +
|
||||
'This is a bug in React.',
|
||||
);
|
||||
}
|
||||
if (suspendedBySet !== undefined && suspendedBySet.size === 0) {
|
||||
if (suspendedBySet.size === 0) {
|
||||
parentSuspenseNode.suspendedBy.delete(asyncInfo.awaited);
|
||||
}
|
||||
if (
|
||||
@@ -3086,24 +3025,6 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function unmountSuspenseChildrenRecursively(
|
||||
contentInstance: DevToolsInstance,
|
||||
stashedSuspenseParent: null | SuspenseNode,
|
||||
stashedSuspensePrevious: null | SuspenseNode,
|
||||
stashedSuspenseRemaining: null | SuspenseNode,
|
||||
): void {
|
||||
// First unmount only the Offscreen boundary. I.e. the main content.
|
||||
unmountInstanceRecursively(contentInstance);
|
||||
|
||||
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
|
||||
// unmount the fallback, unmounting anything in the context of the parent SuspenseNode.
|
||||
// Since the fallback conceptually blocks the parent.
|
||||
reconcilingParentSuspenseNode = stashedSuspenseParent;
|
||||
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
|
||||
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
|
||||
unmountRemainingChildren();
|
||||
}
|
||||
|
||||
function isChildOf(
|
||||
parentInstance: DevToolsInstance,
|
||||
childInstance: DevToolsInstance,
|
||||
@@ -3601,9 +3522,6 @@ export function attach(
|
||||
// Unfortunately if we don't have any DEV time debug info or debug thenables then
|
||||
// we have no meta data to show. However, we still mark this Suspense boundary as
|
||||
// participating in the loading sequence since apparently it can suspend.
|
||||
if (!suspenseNode.hasUniqueSuspenders) {
|
||||
recordSuspenseSuspenders(suspenseNode);
|
||||
}
|
||||
suspenseNode.hasUniqueSuspenders = true;
|
||||
// We have not seen any reason yet for why this suspense node might have been
|
||||
// suspended but it clearly has been at some point. If we later discover a reason
|
||||
@@ -4049,7 +3967,6 @@ export function attach(
|
||||
debug('unmountInstanceRecursively()', instance, reconcilingParent);
|
||||
}
|
||||
|
||||
let shouldPopSuspenseNode = false;
|
||||
const stashedParent = reconcilingParent;
|
||||
const stashedPrevious = previouslyReconciledSibling;
|
||||
const stashedRemaining = remainingReconcilingChildren;
|
||||
@@ -4070,46 +3987,11 @@ export function attach(
|
||||
previouslyReconciledSiblingSuspenseNode = null;
|
||||
remainingReconcilingChildrenSuspenseNodes =
|
||||
instance.suspenseNode.firstChild;
|
||||
|
||||
shouldPopSuspenseNode = true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Unmount the remaining set.
|
||||
if (
|
||||
(instance.kind === FIBER_INSTANCE ||
|
||||
instance.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
instance.data.tag === SuspenseComponent &&
|
||||
OffscreenComponent !== -1
|
||||
) {
|
||||
const fiber = instance.data;
|
||||
const contentFiberInstance = remainingReconcilingChildren;
|
||||
const hydrated = isFiberHydrated(fiber);
|
||||
if (hydrated) {
|
||||
if (contentFiberInstance === null) {
|
||||
throw new Error(
|
||||
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
|
||||
);
|
||||
}
|
||||
|
||||
unmountSuspenseChildrenRecursively(
|
||||
contentFiberInstance,
|
||||
stashedSuspenseParent,
|
||||
stashedSuspensePrevious,
|
||||
stashedSuspenseRemaining,
|
||||
);
|
||||
// unmountSuspenseChildren already popped
|
||||
shouldPopSuspenseNode = false;
|
||||
} else {
|
||||
if (contentFiberInstance !== null) {
|
||||
throw new Error(
|
||||
'A dehydrated Suspense node should not have a content Fiber.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unmountRemainingChildren();
|
||||
}
|
||||
unmountRemainingChildren();
|
||||
removePreviousSuspendedBy(
|
||||
instance,
|
||||
previousSuspendedBy,
|
||||
@@ -4119,7 +4001,7 @@ export function attach(
|
||||
reconcilingParent = stashedParent;
|
||||
previouslyReconciledSibling = stashedPrevious;
|
||||
remainingReconcilingChildren = stashedRemaining;
|
||||
if (shouldPopSuspenseNode) {
|
||||
if (instance.suspenseNode !== null) {
|
||||
reconcilingParentSuspenseNode = stashedSuspenseParent;
|
||||
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
|
||||
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
|
||||
@@ -4380,9 +4262,7 @@ export function attach(
|
||||
virtualLevel + 1,
|
||||
);
|
||||
if ((updateFlags & ShouldResetChildren) !== NoUpdate) {
|
||||
if (!isInDisconnectedSubtree) {
|
||||
recordResetChildren(virtualInstance);
|
||||
}
|
||||
recordResetChildren(virtualInstance);
|
||||
updateFlags &= ~ShouldResetChildren;
|
||||
}
|
||||
removePreviousSuspendedBy(
|
||||
@@ -5169,9 +5049,7 @@ export function attach(
|
||||
// We need to crawl the subtree for closest non-filtered Fibers
|
||||
// so that we can display them in a flat children set.
|
||||
if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) {
|
||||
if (!nextIsHidden && !isInDisconnectedSubtree) {
|
||||
recordResetChildren(fiberInstance);
|
||||
}
|
||||
recordResetChildren(fiberInstance);
|
||||
|
||||
// We've handled the child order change for this Fiber.
|
||||
// Since it's included, there's no need to invalidate parent child order.
|
||||
@@ -5352,7 +5230,7 @@ export function attach(
|
||||
|
||||
mountFiberRecursively(root.current, false);
|
||||
|
||||
flushPendingEvents();
|
||||
flushPendingEvents(root);
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
currentRoot = (null: any);
|
||||
@@ -5392,12 +5270,12 @@ export function attach(
|
||||
root: FiberRoot,
|
||||
priorityLevel: void | number,
|
||||
) {
|
||||
const nextFiber = root.current;
|
||||
const current = root.current;
|
||||
|
||||
let prevFiber: null | Fiber = null;
|
||||
let rootInstance = rootToFiberInstanceMap.get(root);
|
||||
if (!rootInstance) {
|
||||
rootInstance = createFiberInstance(nextFiber);
|
||||
rootInstance = createFiberInstance(current);
|
||||
rootToFiberInstanceMap.set(root, rootInstance);
|
||||
idToDevToolsInstanceMap.set(rootInstance.id, rootInstance);
|
||||
} else {
|
||||
@@ -5436,28 +5314,30 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
const nextIsMounted = nextFiber.child !== null;
|
||||
const prevWasMounted = prevFiber !== null && prevFiber.child !== null;
|
||||
if (!prevWasMounted && nextIsMounted) {
|
||||
// Mount a new root.
|
||||
setRootPseudoKey(currentRoot.id, nextFiber);
|
||||
mountFiberRecursively(nextFiber, false);
|
||||
} else if (prevWasMounted && nextIsMounted) {
|
||||
if (prevFiber === null) {
|
||||
throw new Error(
|
||||
'Expected a previous Fiber when updating an existing root.',
|
||||
);
|
||||
if (prevFiber !== null) {
|
||||
// TODO: relying on this seems a bit fishy.
|
||||
const wasMounted =
|
||||
prevFiber.memoizedState != null &&
|
||||
prevFiber.memoizedState.element != null;
|
||||
const isMounted =
|
||||
current.memoizedState != null && current.memoizedState.element != null;
|
||||
if (!wasMounted && isMounted) {
|
||||
// Mount a new root.
|
||||
setRootPseudoKey(currentRoot.id, current);
|
||||
mountFiberRecursively(current, false);
|
||||
} else if (wasMounted && isMounted) {
|
||||
// Update an existing root.
|
||||
updateFiberRecursively(rootInstance, current, prevFiber, false);
|
||||
} else if (wasMounted && !isMounted) {
|
||||
// Unmount an existing root.
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
removeRootPseudoKey(currentRoot.id);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
}
|
||||
// Update an existing root.
|
||||
updateFiberRecursively(rootInstance, nextFiber, prevFiber, false);
|
||||
} else if (prevWasMounted && !nextIsMounted) {
|
||||
// Unmount an existing root.
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
removeRootPseudoKey(currentRoot.id);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
} else if (!prevWasMounted && !nextIsMounted) {
|
||||
// We don't need this root anymore.
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
} else {
|
||||
// Mount a new root.
|
||||
setRootPseudoKey(currentRoot.id, current);
|
||||
mountFiberRecursively(current, false);
|
||||
}
|
||||
|
||||
if (isProfiling && isProfilingSupported) {
|
||||
@@ -5481,7 +5361,7 @@ export function attach(
|
||||
}
|
||||
|
||||
// We're done here.
|
||||
flushPendingEvents();
|
||||
flushPendingEvents(root);
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
|
||||
@@ -5841,7 +5721,6 @@ export function attach(
|
||||
|
||||
function getSuspendedByOfSuspenseNode(
|
||||
suspenseNode: SuspenseNode,
|
||||
filterByChildInstance: null | DevToolsInstance, // only include suspended by instances in this subtree
|
||||
): Array<SerializedAsyncInfo> {
|
||||
// Collect all ReactAsyncInfo that was suspending this SuspenseNode but
|
||||
// isn't also in any parent set.
|
||||
@@ -5854,15 +5733,6 @@ export function attach(
|
||||
// to a specific instance will have those appear in order of when that instance was discovered.
|
||||
let hooksCacheKey: null | DevToolsInstance = null;
|
||||
let hooksCache: null | HooksTree = null;
|
||||
// Collect the stream entries with the highest byte offset and end time.
|
||||
const streamEntries: Map<
|
||||
Promise<mixed>,
|
||||
{
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
instance: DevToolsInstance,
|
||||
hooks: null | HooksTree,
|
||||
},
|
||||
> = new Map();
|
||||
suspenseNode.suspendedBy.forEach((set, ioInfo) => {
|
||||
let parentNode = suspenseNode.parent;
|
||||
while (parentNode !== null) {
|
||||
@@ -5877,30 +5747,8 @@ export function attach(
|
||||
if (set.size === 0) {
|
||||
return;
|
||||
}
|
||||
let firstInstance: null | DevToolsInstance = null;
|
||||
if (filterByChildInstance === null) {
|
||||
firstInstance = (set.values().next().value: any);
|
||||
} else {
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const childInstance of set.values()) {
|
||||
if (firstInstance === null) {
|
||||
firstInstance = childInstance;
|
||||
}
|
||||
if (
|
||||
childInstance !== filterByChildInstance &&
|
||||
!isChildOf(
|
||||
filterByChildInstance,
|
||||
childInstance,
|
||||
suspenseNode.instance,
|
||||
)
|
||||
) {
|
||||
// Something suspended on this outside the filtered instance. That means that
|
||||
// it is not unique to just this filtered instance so we skip including it.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstInstance !== null && firstInstance.suspendedBy !== null) {
|
||||
const firstInstance: DevToolsInstance = (set.values().next().value: any);
|
||||
if (firstInstance.suspendedBy !== null) {
|
||||
const asyncInfo = getAwaitInSuspendedByFromIO(
|
||||
firstInstance.suspendedBy,
|
||||
ioInfo,
|
||||
@@ -5923,113 +5771,13 @@ export function attach(
|
||||
}
|
||||
}
|
||||
}
|
||||
const newIO = asyncInfo.awaited;
|
||||
if (newIO.name === 'RSC stream' && newIO.value != null) {
|
||||
const streamPromise = newIO.value;
|
||||
// Special case RSC stream entries to pick the last entry keyed by the stream.
|
||||
const existingEntry = streamEntries.get(streamPromise);
|
||||
if (existingEntry === undefined) {
|
||||
streamEntries.set(streamPromise, {
|
||||
asyncInfo,
|
||||
instance: firstInstance,
|
||||
hooks,
|
||||
});
|
||||
} else {
|
||||
const existingIO = existingEntry.asyncInfo.awaited;
|
||||
if (
|
||||
newIO !== existingIO &&
|
||||
((newIO.byteSize !== undefined &&
|
||||
existingIO.byteSize !== undefined &&
|
||||
newIO.byteSize > existingIO.byteSize) ||
|
||||
newIO.end > existingIO.end)
|
||||
) {
|
||||
// The new entry is later in the stream that the old entry. Replace it.
|
||||
existingEntry.asyncInfo = asyncInfo;
|
||||
existingEntry.instance = firstInstance;
|
||||
existingEntry.hooks = hooks;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
|
||||
}
|
||||
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
|
||||
}
|
||||
}
|
||||
});
|
||||
// Add any deduped stream entries.
|
||||
streamEntries.forEach(({asyncInfo, instance, hooks}) => {
|
||||
result.push(serializeAsyncInfo(asyncInfo, instance, hooks));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSuspendedByOfInstance(
|
||||
devtoolsInstance: DevToolsInstance,
|
||||
hooks: null | HooksTree,
|
||||
): Array<SerializedAsyncInfo> {
|
||||
const suspendedBy = devtoolsInstance.suspendedBy;
|
||||
if (suspendedBy === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const foundIOEntries: Set<ReactIOInfo> = new Set();
|
||||
const streamEntries: Map<Promise<mixed>, ReactAsyncInfo> = new Map();
|
||||
const result: Array<SerializedAsyncInfo> = [];
|
||||
for (let i = 0; i < suspendedBy.length; i++) {
|
||||
const asyncInfo = suspendedBy[i];
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
if (foundIOEntries.has(ioInfo)) {
|
||||
// We have already added this I/O entry to the result. We can dedupe it.
|
||||
// This can happen when an instance depends on the same data in mutliple places.
|
||||
continue;
|
||||
}
|
||||
foundIOEntries.add(ioInfo);
|
||||
if (ioInfo.name === 'RSC stream' && ioInfo.value != null) {
|
||||
const streamPromise = ioInfo.value;
|
||||
// Special case RSC stream entries to pick the last entry keyed by the stream.
|
||||
const existingEntry = streamEntries.get(streamPromise);
|
||||
if (existingEntry === undefined) {
|
||||
streamEntries.set(streamPromise, asyncInfo);
|
||||
} else {
|
||||
const existingIO = existingEntry.awaited;
|
||||
if (
|
||||
ioInfo !== existingIO &&
|
||||
((ioInfo.byteSize !== undefined &&
|
||||
existingIO.byteSize !== undefined &&
|
||||
ioInfo.byteSize > existingIO.byteSize) ||
|
||||
ioInfo.end > existingIO.end)
|
||||
) {
|
||||
// The new entry is later in the stream that the old entry. Replace it.
|
||||
streamEntries.set(streamPromise, asyncInfo);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
|
||||
}
|
||||
}
|
||||
// Add any deduped stream entries.
|
||||
streamEntries.forEach(asyncInfo => {
|
||||
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSuspendedByOfInstanceSubtree(
|
||||
devtoolsInstance: DevToolsInstance,
|
||||
): Array<SerializedAsyncInfo> {
|
||||
// Get everything suspending below this instance down to the next Suspense node.
|
||||
// First find the parent Suspense boundary which will have accumulated everything
|
||||
let suspenseParentInstance = devtoolsInstance;
|
||||
while (suspenseParentInstance.suspenseNode === null) {
|
||||
if (suspenseParentInstance.parent === null) {
|
||||
// We don't expect to hit this. We should always find the root.
|
||||
return [];
|
||||
}
|
||||
suspenseParentInstance = suspenseParentInstance.parent;
|
||||
}
|
||||
const suspenseNode: SuspenseNode = suspenseParentInstance.suspenseNode;
|
||||
return getSuspendedByOfSuspenseNode(suspenseNode, devtoolsInstance);
|
||||
}
|
||||
|
||||
const FALLBACK_THROTTLE_MS: number = 300;
|
||||
|
||||
function getSuspendedByRange(
|
||||
@@ -6543,17 +6291,17 @@ export function attach(
|
||||
fiberInstance.suspenseNode !== null
|
||||
? // If this is a Suspense boundary, then we include everything in the subtree that might suspend
|
||||
// this boundary down to the next Suspense boundary.
|
||||
getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode, null)
|
||||
: tag === ActivityComponent
|
||||
? // For Activity components we show everything that suspends the subtree down to the next boundary
|
||||
// so that you can see what suspends a Transition at that level.
|
||||
getSuspendedByOfInstanceSubtree(fiberInstance)
|
||||
: // This set is an edge case where if you pass a promise to a Client Component into a children
|
||||
// position without a Server Component as the direct parent. E.g. <div>{promise}</div>
|
||||
// In this case, this becomes associated with the Client/Host Component where as normally
|
||||
// you'd expect these to be associated with the Server Component that awaited the data.
|
||||
// TODO: Prepend other suspense sources like css, images and use().
|
||||
getSuspendedByOfInstance(fiberInstance, hooks);
|
||||
getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode)
|
||||
: // This set is an edge case where if you pass a promise to a Client Component into a children
|
||||
// position without a Server Component as the direct parent. E.g. <div>{promise}</div>
|
||||
// In this case, this becomes associated with the Client/Host Component where as normally
|
||||
// you'd expect these to be associated with the Server Component that awaited the data.
|
||||
// TODO: Prepend other suspense sources like css, images and use().
|
||||
fiberInstance.suspendedBy === null
|
||||
? []
|
||||
: fiberInstance.suspendedBy.map(info =>
|
||||
serializeAsyncInfo(info, fiberInstance, hooks),
|
||||
);
|
||||
const suspendedByRange = getSuspendedByRange(
|
||||
getNearestSuspenseNode(fiberInstance),
|
||||
);
|
||||
@@ -6698,7 +6446,7 @@ export function attach(
|
||||
|
||||
const isSuspended = null;
|
||||
// Things that Suspended this Server Component (use(), awaits and direct child promises)
|
||||
const suspendedBy = getSuspendedByOfInstance(virtualInstance, null);
|
||||
const suspendedBy = virtualInstance.suspendedBy;
|
||||
const suspendedByRange = getSuspendedByRange(
|
||||
getNearestSuspenseNode(virtualInstance),
|
||||
);
|
||||
@@ -6749,7 +6497,12 @@ export function attach(
|
||||
? []
|
||||
: Array.from(componentLogsEntry.warnings.entries()),
|
||||
|
||||
suspendedBy: suspendedBy,
|
||||
suspendedBy:
|
||||
suspendedBy === null
|
||||
? []
|
||||
: suspendedBy.map(info =>
|
||||
serializeAsyncInfo(info, virtualInstance, null),
|
||||
),
|
||||
suspendedByRange: suspendedByRange,
|
||||
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ export const SUSPENSE_TREE_OPERATION_ADD = 8;
|
||||
export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
|
||||
export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
|
||||
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
|
||||
export const SUSPENSE_TREE_OPERATION_SUSPENDERS = 12;
|
||||
|
||||
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
|
||||
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;
|
||||
|
||||
122
packages/react-devtools-shared/src/devtools/store.js
vendored
122
packages/react-devtools-shared/src/devtools/store.js
vendored
@@ -24,7 +24,6 @@ import {
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
SUSPENSE_TREE_OPERATION_RESIZE,
|
||||
SUSPENSE_TREE_OPERATION_SUSPENDERS,
|
||||
} from '../constants';
|
||||
import {ElementTypeRoot} from '../frontend/types';
|
||||
import {
|
||||
@@ -112,7 +111,7 @@ export default class Store extends EventEmitter<{
|
||||
roots: [],
|
||||
rootSupportsBasicProfiling: [],
|
||||
rootSupportsTimelineProfiling: [],
|
||||
suspenseTreeMutated: [[Map<SuspenseNode['id'], SuspenseNode['id']>]],
|
||||
suspenseTreeMutated: [],
|
||||
supportsNativeStyleEditor: [],
|
||||
supportsReloadAndProfile: [],
|
||||
unsupportedBridgeProtocolDetected: [],
|
||||
@@ -848,83 +847,6 @@ export default class Store extends EventEmitter<{
|
||||
return list;
|
||||
}
|
||||
|
||||
getSuspenseLineage(
|
||||
suspenseID: SuspenseNode['id'],
|
||||
): $ReadOnlyArray<SuspenseNode['id']> {
|
||||
const lineage: Array<SuspenseNode['id']> = [];
|
||||
let next: null | SuspenseNode = this.getSuspenseByID(suspenseID);
|
||||
while (next !== null) {
|
||||
if (next.parentID === 0) {
|
||||
next = null;
|
||||
} else {
|
||||
lineage.unshift(next.id);
|
||||
next = this.getSuspenseByID(next.parentID);
|
||||
}
|
||||
}
|
||||
|
||||
return lineage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link getRootIDForElement} but should be used for traversing Suspense since it works with disconnected nodes.
|
||||
*/
|
||||
getSuspenseRootIDForSuspense(id: SuspenseNode['id']): number | null {
|
||||
let current = this._idToSuspense.get(id);
|
||||
while (current !== undefined) {
|
||||
if (current.parentID === 0) {
|
||||
return current.id;
|
||||
} else {
|
||||
current = this._idToSuspense.get(current.parentID);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rootID
|
||||
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
|
||||
*/
|
||||
getSuspendableDocumentOrderSuspense(
|
||||
rootID: Element['id'] | void,
|
||||
uniqueSuspendersOnly: boolean,
|
||||
): $ReadOnlyArray<SuspenseNode['id']> {
|
||||
if (rootID === undefined) {
|
||||
return [];
|
||||
}
|
||||
const root = this.getElementByID(rootID);
|
||||
if (root === null) {
|
||||
return [];
|
||||
}
|
||||
if (!this.supportsTogglingSuspense(root.id)) {
|
||||
return [];
|
||||
}
|
||||
const list: SuspenseNode['id'][] = [];
|
||||
const suspense = this.getSuspenseByID(root.id);
|
||||
if (suspense !== null) {
|
||||
const stack = [suspense];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (current === undefined) {
|
||||
continue;
|
||||
}
|
||||
// Include the root even if we won't show it suspended (because that's just blank).
|
||||
// You should be able to see what suspended the shell.
|
||||
if (!uniqueSuspendersOnly || current.hasUniqueSuspenders) {
|
||||
list.push(current.id);
|
||||
}
|
||||
// Add children in reverse order to maintain document order
|
||||
for (let j = current.children.length - 1; j >= 0; j--) {
|
||||
const childSuspense = this.getSuspenseByID(current.children[j]);
|
||||
if (childSuspense !== null) {
|
||||
stack.push(childSuspense);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
getRendererIDForElement(id: number): number | null {
|
||||
let current = this._idToElement.get(id);
|
||||
while (current !== undefined) {
|
||||
@@ -1108,8 +1030,6 @@ export default class Store extends EventEmitter<{
|
||||
const addedElementIDs: Array<number> = [];
|
||||
// This is a mapping of removed ID -> parent ID:
|
||||
const removedElementIDs: Map<number, number> = new Map();
|
||||
const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =
|
||||
new Map();
|
||||
// We'll use the parent ID to adjust selection if it gets deleted.
|
||||
|
||||
let i = 2;
|
||||
@@ -1588,7 +1508,6 @@ export default class Store extends EventEmitter<{
|
||||
children: [],
|
||||
name,
|
||||
rects,
|
||||
hasUniqueSuspenders: false,
|
||||
});
|
||||
|
||||
hasSuspenseTreeChanged = true;
|
||||
@@ -1622,7 +1541,6 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
|
||||
this._idToSuspense.delete(id);
|
||||
removedSuspenseIDs.set(id, parentID);
|
||||
|
||||
let parentSuspense: ?SuspenseNode = null;
|
||||
if (parentID === 0) {
|
||||
@@ -1758,42 +1676,6 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
break;
|
||||
}
|
||||
case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
|
||||
const changeLength = operations[i + 1];
|
||||
i += 2;
|
||||
|
||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||
const id = operations[i];
|
||||
const hasUniqueSuspenders = operations[i + 1] === 1;
|
||||
const suspense = this._idToSuspense.get(id);
|
||||
|
||||
if (suspense === undefined) {
|
||||
this._throwAndEmitError(
|
||||
Error(
|
||||
`Cannot update suspenders of suspense node "${id}" because no matching node was found in the Store.`,
|
||||
),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
i += 2;
|
||||
|
||||
if (__DEBUG__) {
|
||||
const previousHasUniqueSuspenders = suspense.hasUniqueSuspenders;
|
||||
debug(
|
||||
'Suspender changes',
|
||||
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} (was ${String(previousHasUniqueSuspenders)})`,
|
||||
);
|
||||
}
|
||||
|
||||
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
|
||||
}
|
||||
|
||||
hasSuspenseTreeChanged = true;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this._throwAndEmitError(
|
||||
new UnsupportedBridgeOperationError(
|
||||
@@ -1866,7 +1748,7 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
|
||||
if (hasSuspenseTreeChanged) {
|
||||
this.emit('suspenseTreeMutated', [removedSuspenseIDs]);
|
||||
this.emit('suspenseTreeMutated');
|
||||
}
|
||||
|
||||
if (__DEBUG__) {
|
||||
|
||||
@@ -52,7 +52,7 @@ type Props = {
|
||||
type: IconType,
|
||||
};
|
||||
|
||||
const panelIcons = '0 -960 960 820';
|
||||
const materialIconsViewBox = '0 -960 960 960';
|
||||
export default function ButtonIcon({className = '', type}: Props): React.Node {
|
||||
let pathData = null;
|
||||
let viewBox = '0 0 24 24';
|
||||
@@ -131,27 +131,27 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
|
||||
break;
|
||||
case 'panel-left-close':
|
||||
pathData = PATH_MATERIAL_PANEL_LEFT_CLOSE;
|
||||
viewBox = panelIcons;
|
||||
viewBox = materialIconsViewBox;
|
||||
break;
|
||||
case 'panel-left-open':
|
||||
pathData = PATH_MATERIAL_PANEL_LEFT_OPEN;
|
||||
viewBox = panelIcons;
|
||||
viewBox = materialIconsViewBox;
|
||||
break;
|
||||
case 'panel-right-close':
|
||||
pathData = PATH_MATERIAL_PANEL_RIGHT_CLOSE;
|
||||
viewBox = panelIcons;
|
||||
viewBox = materialIconsViewBox;
|
||||
break;
|
||||
case 'panel-right-open':
|
||||
pathData = PATH_MATERIAL_PANEL_RIGHT_OPEN;
|
||||
viewBox = panelIcons;
|
||||
viewBox = materialIconsViewBox;
|
||||
break;
|
||||
case 'panel-bottom-open':
|
||||
pathData = PATH_MATERIAL_PANEL_BOTTOM_OPEN;
|
||||
viewBox = panelIcons;
|
||||
viewBox = materialIconsViewBox;
|
||||
break;
|
||||
case 'panel-bottom-close':
|
||||
pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE;
|
||||
viewBox = panelIcons;
|
||||
viewBox = materialIconsViewBox;
|
||||
break;
|
||||
case 'suspend':
|
||||
pathData = PATH_SUSPEND;
|
||||
|
||||
@@ -14,17 +14,8 @@ import styles from './Badge.css';
|
||||
type Props = {
|
||||
className?: string,
|
||||
children: React$Node,
|
||||
...
|
||||
};
|
||||
|
||||
export default function Badge({
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}: Props): React.Node {
|
||||
return (
|
||||
<div {...props} className={`${styles.Badge} ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
export default function Badge({className = '', children}: Props): React.Node {
|
||||
return <div className={`${styles.Badge} ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
@@ -8,34 +8,22 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {useState, useContext, useCallback} from 'react';
|
||||
import {useContext} from 'react';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
|
||||
import SearchInput from 'react-devtools-shared/src/devtools/views/SearchInput';
|
||||
import {
|
||||
TreeDispatcherContext,
|
||||
TreeStateContext,
|
||||
} from 'react-devtools-shared/src/devtools/views/Components/TreeContext';
|
||||
import SearchInput from '../SearchInput';
|
||||
|
||||
export default function ComponentSearchInput(): React.Node {
|
||||
const [localSearchQuery, setLocalSearchQuery] = useState('');
|
||||
const {searchIndex, searchResults} = useContext(TreeStateContext);
|
||||
const transitionDispatch = useContext(TreeDispatcherContext);
|
||||
type Props = {};
|
||||
|
||||
const search = useCallback(
|
||||
(text: string) => {
|
||||
setLocalSearchQuery(text);
|
||||
transitionDispatch({type: 'SET_SEARCH_TEXT', payload: text});
|
||||
},
|
||||
[setLocalSearchQuery, transitionDispatch],
|
||||
);
|
||||
const goToNextResult = useCallback(
|
||||
() => transitionDispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}),
|
||||
[transitionDispatch],
|
||||
);
|
||||
const goToPreviousResult = useCallback(
|
||||
() => transitionDispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}),
|
||||
[transitionDispatch],
|
||||
);
|
||||
export default function ComponentSearchInput(props: Props): React.Node {
|
||||
const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const search = (text: string) =>
|
||||
dispatch({type: 'SET_SEARCH_TEXT', payload: text});
|
||||
const goToNextResult = () => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'});
|
||||
const goToPreviousResult = () =>
|
||||
dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'});
|
||||
|
||||
return (
|
||||
<SearchInput
|
||||
@@ -45,7 +33,7 @@ export default function ComponentSearchInput(): React.Node {
|
||||
search={search}
|
||||
searchIndex={searchIndex}
|
||||
searchResultsCount={searchResults.length}
|
||||
searchText={localSearchQuery}
|
||||
searchText={searchText}
|
||||
testName="ComponentSearchInput"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {Element as ElementType} from 'react-devtools-shared/src/frontend/ty
|
||||
import styles from './Element.css';
|
||||
import Icon from '../Icon';
|
||||
import {useChangeOwnerAction} from './OwnersListContext';
|
||||
import Tooltip from './reach-ui/tooltip';
|
||||
|
||||
type Props = {
|
||||
data: ItemData,
|
||||
@@ -232,16 +231,15 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
/>
|
||||
)}
|
||||
{showStrictModeBadge && (
|
||||
<Tooltip label="This component is not running in StrictMode.">
|
||||
<Icon
|
||||
className={
|
||||
isSelected && treeFocused
|
||||
? styles.StrictModeContrast
|
||||
: styles.StrictMode
|
||||
}
|
||||
type="strict-mode-non-compliant"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Icon
|
||||
className={
|
||||
isSelected && treeFocused
|
||||
? styles.StrictModeContrast
|
||||
: styles.StrictMode
|
||||
}
|
||||
title="This component is not running in StrictMode."
|
||||
type="strict-mode-non-compliant"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,3 +11,11 @@
|
||||
position: absolute;
|
||||
right: 0.25em;
|
||||
}
|
||||
|
||||
.ForgetToggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ForgetToggle > span { /* targets .ToggleContent */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as React from 'react';
|
||||
|
||||
import Badge from './Badge';
|
||||
import IndexableDisplayName from './IndexableDisplayName';
|
||||
import Tooltip from './reach-ui/tooltip';
|
||||
import Toggle from '../Toggle';
|
||||
|
||||
import styles from './ForgetBadge.css';
|
||||
|
||||
@@ -40,11 +40,12 @@ export default function ForgetBadge(props: Props): React.Node {
|
||||
'Memo'
|
||||
);
|
||||
|
||||
const onChange = () => {};
|
||||
const title =
|
||||
'✨ This component has been auto-memoized by the React Compiler.';
|
||||
return (
|
||||
<Tooltip label={title}>
|
||||
<Toggle onChange={onChange} className={styles.ForgetToggle} title={title}>
|
||||
<Badge className={`${styles.Root} ${className}`}>{innerView}</Badge>
|
||||
</Tooltip>
|
||||
</Toggle>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import InspectedElementViewSourceButton from './InspectedElementViewSourceButton
|
||||
import useEditorURL from '../useEditorURL';
|
||||
|
||||
import styles from './InspectedElement.css';
|
||||
import Tooltip from './reach-ui/tooltip';
|
||||
|
||||
export type Props = {};
|
||||
|
||||
@@ -193,15 +192,14 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
let strictModeBadge = null;
|
||||
if (element.isStrictModeNonCompliant) {
|
||||
strictModeBadge = (
|
||||
<Tooltip label="This component is not running in StrictMode. Click to learn more.">
|
||||
<a
|
||||
className={styles.StrictModeNonCompliant}
|
||||
href="https://react.dev/reference/react/StrictMode"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank">
|
||||
<Icon type="strict-mode-non-compliant" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<a
|
||||
className={styles.StrictModeNonCompliant}
|
||||
href="https://react.dev/reference/react/StrictMode"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title="This component is not running in StrictMode. Click to learn more.">
|
||||
<Icon type="strict-mode-non-compliant" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user