Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52d241b0c4 | ||
|
|
13c3ffcea8 | ||
|
|
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',
|
||||
|
||||
@@ -520,7 +520,8 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.AutomaticEffectDependencies:
|
||||
case ErrorCategory.CapitalizedCalls:
|
||||
case ErrorCategory.Config:
|
||||
case ErrorCategory.EffectStateDerivationCalculateInRender:
|
||||
case ErrorCategory.EffectDerivationDeriveInRender:
|
||||
case ErrorCategory.EffectDerivationShadowingParentState:
|
||||
case ErrorCategory.EffectSetState:
|
||||
case ErrorCategory.ErrorBoundaries:
|
||||
case ErrorCategory.Factories:
|
||||
@@ -615,9 +616,13 @@ export enum ErrorCategory {
|
||||
*/
|
||||
EffectSetState = 'EffectSetState',
|
||||
/**
|
||||
* Checks for no deriving state in effects, solved by calculate in render
|
||||
* Checks for derived state in effects that could be calculated in render
|
||||
*/
|
||||
EffectStateDerivationCalculateInRender = 'EffectStateDerivationCalculateInRender',
|
||||
EffectDerivationDeriveInRender = 'EffectDerivationDeriveInRender',
|
||||
/**
|
||||
* Checks for derived state in effects that could be hoisted to parent
|
||||
*/
|
||||
EffectDerivationShadowingParentState = 'EffectDerivationShadowingParentState',
|
||||
/**
|
||||
* Validates against try/catch in place of error boundaries
|
||||
*/
|
||||
@@ -754,13 +759,23 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectStateDerivationCalculateInRender: {
|
||||
case ErrorCategory.EffectDerivationDeriveInRender: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'no-deriving-state-in-effects-calculate-in-render',
|
||||
name: 'effect-derive-in-render',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
'Validates if a useEffect is deriving state from props and/or local state that could be calculated in render.',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationShadowingParentState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'effect-shadow-parent-state',
|
||||
description:
|
||||
'Validates if a useEffect is deriving state from parent state and if the component is updating the shadowed state locally.',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,109 +5,93 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BasicBlock,
|
||||
GeneratedSource,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
isSetStateType,
|
||||
Place,
|
||||
isUseStateType,
|
||||
Effect,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
FunctionExpression,
|
||||
BlockId,
|
||||
SourceLocation,
|
||||
CallExpression,
|
||||
isUseStateType,
|
||||
isUseRefType,
|
||||
GeneratedSource,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {eachInstructionOperand, eachInstructionLValue} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
|
||||
type SetStateCall = {
|
||||
loc: SourceLocation;
|
||||
derivedDep: DerivationMetadata;
|
||||
setStateId: IdentifierId;
|
||||
};
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sourcesIds: Set<IdentifierId>;
|
||||
};
|
||||
|
||||
type DerivationCache = Map<IdentifierId, DerivationMetadata>;
|
||||
|
||||
type SetStateCallCache = Map<string | undefined | null, Array<Place>>;
|
||||
|
||||
type FunctionExpressionsCache = Map<IdentifierId, FunctionExpression>;
|
||||
|
||||
type DerivedSetStateCall = {
|
||||
value: CallExpression;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
sources: Array<Place>;
|
||||
};
|
||||
|
||||
type ErrorMetadata = {
|
||||
derivedComputationDetails: string;
|
||||
type: TypeOfValue;
|
||||
description: string | undefined;
|
||||
loc: SourceLocation;
|
||||
setStateName: string | undefined | null;
|
||||
derivedDepsNames: Array<string>;
|
||||
};
|
||||
|
||||
const DERIVE_IN_RENDER_REASON =
|
||||
'You might net need an effect. Derive values in render, not effects.';
|
||||
|
||||
const DERIVE_IN_RENDER_DETAIL_MESSAGE =
|
||||
'This should be computed during render, not in an effect';
|
||||
|
||||
const DERIVE_IN_RENDER_DESCRIPTION =
|
||||
'State derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
*
|
||||
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // 🔴 Avoid: redundant state and unnecessary Effect
|
||||
* const [fullName, setFullName] = useState('');
|
||||
* useEffect(() => {
|
||||
* setFullName(firstName + ' ' + lastName);
|
||||
* }, [firstName, lastName]);
|
||||
* ```
|
||||
*
|
||||
* Instead use:
|
||||
*
|
||||
* ```
|
||||
* // ✅ Good: calculated during rendering
|
||||
* const fullName = firstName + ' ' + lastName;
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const derivationCache: DerivationCache = new Map();
|
||||
const setStateCallCache: SetStateCallCache = new Map();
|
||||
const effectSetStateCache: SetStateCallCache = new Map();
|
||||
const functionExpressionsCache: FunctionExpressionsCache = new Map();
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const derivationCache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
const shadowingUseState: Map<string, Array<SourceLocation>> = new Map();
|
||||
|
||||
const stateDerivationErrors: Array<ErrorMetadata> = [];
|
||||
const effectSetStates: Map<
|
||||
string | undefined | null,
|
||||
Array<Place>
|
||||
> = new Map();
|
||||
const setStateCalls: Map<string | undefined | null, Array<Place>> = new Map();
|
||||
|
||||
parseFNParameters(fn, derivationCache);
|
||||
const errors: Array<ErrorMetadata> = [];
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
parseBlockPhi(block, derivationCache);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
parseInstr(
|
||||
instr,
|
||||
derivationCache,
|
||||
setStateCallCache,
|
||||
effectSetStateCache,
|
||||
functionExpressionsCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const compilerError = generateCompilerErrors(stateDerivationErrors);
|
||||
|
||||
if (compilerError.hasErrors()) {
|
||||
throw compilerError;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFNParameters(fn: HIRFunction, derivationCache: DerivationCache) {
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
derivationCache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
sources: [param],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
@@ -117,35 +101,166 @@ function parseFNParameters(fn: HIRFunction, derivationCache: DerivationCache) {
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
derivationCache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
sources: [props],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseBlockPhi(
|
||||
block: BasicBlock,
|
||||
derivationCache: DerivationCache,
|
||||
): void {
|
||||
for (const phi of block.phis) {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let sourcesIds: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const operandMetadata = derivationCache.get(operand.identifier.id);
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
parseBlockPhi(block, derivationCache);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
|
||||
parseInstr(instr, derivationCache, setStateCalls, shadowingUseState);
|
||||
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
candidateDependencies.set(lvalue.identifier.id, value);
|
||||
} else if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
const deps = candidateDependencies.get(value.args[1].identifier.id);
|
||||
if (
|
||||
effectFunction != null &&
|
||||
deps != null &&
|
||||
deps.elements.length !== 0 &&
|
||||
deps.elements.every(element => element.kind === 'Identifier')
|
||||
) {
|
||||
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
|
||||
CompilerError.invariant(dep.kind === 'Identifier', {
|
||||
reason: `Dependency is checked as a place above`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'this is checked as a place above',
|
||||
},
|
||||
],
|
||||
});
|
||||
return locals.get(dep.identifier.id) ?? dep.identifier.id;
|
||||
});
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
derivationCache,
|
||||
effectSetStates,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
sourcesIds.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
addDerivationEntry(phi.place, sourcesIds, typeOfValue, derivationCache);
|
||||
}
|
||||
}
|
||||
|
||||
const compilerError = generateCompilerError(
|
||||
setStateCalls,
|
||||
effectSetStates,
|
||||
shadowingUseState,
|
||||
errors,
|
||||
);
|
||||
|
||||
if (compilerError.hasErrors()) {
|
||||
throw compilerError;
|
||||
}
|
||||
}
|
||||
|
||||
function generateCompilerError(
|
||||
setStateCalls: Map<string | undefined | null, Array<Place>>,
|
||||
effectSetStates: Map<string | undefined | null, Array<Place>>,
|
||||
shadowingUseState: Map<string, Array<SourceLocation>>,
|
||||
errors: Array<ErrorMetadata>,
|
||||
): CompilerError {
|
||||
const throwableErrors = new CompilerError();
|
||||
for (const error of errors) {
|
||||
let compilerDiagnostic: CompilerDiagnostic | undefined = undefined;
|
||||
|
||||
/*
|
||||
* 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.type !== 'fromState'
|
||||
) {
|
||||
compilerDiagnostic = CompilerDiagnostic.create({
|
||||
description: `The setState within a useEffect is deriving from ${error.description}. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render`,
|
||||
category: ErrorCategory.EffectDerivationShadowingParentState,
|
||||
reason:
|
||||
'You might not need an effect. Local state shadows parent state.',
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: error.loc,
|
||||
message: `this derives values from props ${error.type === 'fromPropsOrState' ? 'and local state ' : ''}to synchronize state`,
|
||||
});
|
||||
|
||||
for (const derivedDep of error.derivedDepsNames) {
|
||||
if (shadowingUseState.has(derivedDep)) {
|
||||
for (const loc of shadowingUseState.get(derivedDep)!) {
|
||||
compilerDiagnostic.withDetails({
|
||||
kind: 'error',
|
||||
loc: loc,
|
||||
message: `this useState shadows ${derivedDep}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, setStateCallArray] of effectSetStates) {
|
||||
if (setStateCallArray.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nonUseEffectSetStateCalls = setStateCalls.get(key);
|
||||
if (nonUseEffectSetStateCalls) {
|
||||
for (const place of nonUseEffectSetStateCalls) {
|
||||
if (!setStateCallArray.includes(place)) {
|
||||
compilerDiagnostic.withDetails({
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message:
|
||||
'this setState updates the shadowed state, but should call an onChange event from the parent',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
compilerDiagnostic = CompilerDiagnostic.create({
|
||||
description: `${error.description ? error.description.charAt(0).toUpperCase() + error.description.slice(1) : ''}. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user`,
|
||||
category: ErrorCategory.EffectDerivationDeriveInRender,
|
||||
reason:
|
||||
'You might not need an effect. Derive values in render, not effects.',
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: error.loc,
|
||||
message: 'This should be computed during render, not in an effect',
|
||||
});
|
||||
}
|
||||
|
||||
if (compilerDiagnostic) {
|
||||
throwableErrors.pushDiagnostic(compilerDiagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
return throwableErrors;
|
||||
}
|
||||
|
||||
function joinValue(
|
||||
@@ -155,146 +270,231 @@ function joinValue(
|
||||
if (lvalueType === 'ignored') return valueType;
|
||||
if (valueType === 'ignored') return lvalueType;
|
||||
if (lvalueType === valueType) return lvalueType;
|
||||
return 'fromPropsAndState';
|
||||
return 'fromPropsOrState';
|
||||
}
|
||||
|
||||
function addDerivationEntry(
|
||||
derivedVar: Place,
|
||||
sourcesIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
derivationCache: DerivationCache,
|
||||
function updateDerivationMetadata(
|
||||
target: Place,
|
||||
sources: Array<DerivationMetadata> | undefined,
|
||||
typeOfValue: TypeOfValue | undefined,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: derivedVar,
|
||||
sourcesIds: new Set(),
|
||||
place: target,
|
||||
sources: [],
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sourcesIds !== undefined) {
|
||||
for (const id of sourcesIds) {
|
||||
const sourcePlace = derivationCache.get(id)?.place;
|
||||
|
||||
if (sourcePlace === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sources !== undefined) {
|
||||
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 (
|
||||
sourcePlace.identifier.name === null ||
|
||||
sourcePlace.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
} else {
|
||||
newValue.sourcesIds.add(sourcePlace.identifier.id);
|
||||
for (const place of source.sources) {
|
||||
if (
|
||||
place.identifier.name === null ||
|
||||
place.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
newValue.sources.push(target);
|
||||
} else {
|
||||
newValue.sources.push(place);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
derivationCache.set(derivedVar.identifier.id, newValue);
|
||||
derivationCache.set(target.identifier.id, newValue);
|
||||
}
|
||||
|
||||
function parseInstr(
|
||||
instr: Instruction,
|
||||
derivationCache: DerivationCache,
|
||||
setStateCallCache: SetStateCallCache,
|
||||
effectSetStateCache: SetStateCallCache,
|
||||
functionExpressionsCache: FunctionExpressionsCache,
|
||||
stateDerivationErrors: Array<ErrorMetadata>,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
setStateCalls: Map<string | undefined | null, Array<Place>>,
|
||||
shadowingUseState: Map<string, Array<SourceLocation>>,
|
||||
): void {
|
||||
const {value, lvalue} = instr;
|
||||
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
|
||||
// Recursively parse function expressions
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
|
||||
let sources: Array<DerivationMetadata> = [];
|
||||
if (instr.value.kind === 'FunctionExpression') {
|
||||
for (const [, block] of instr.value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
functionExpressionsCache.set(lvalue.identifier.id, value);
|
||||
|
||||
parseInstr(
|
||||
instr,
|
||||
derivationCache,
|
||||
setStateCallCache,
|
||||
effectSetStateCache,
|
||||
functionExpressionsCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
parseInstr(instr, derivationCache, setStateCalls, shadowingUseState);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Record setState calls
|
||||
else if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isSetStateType(value.callee.identifier)
|
||||
} else 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
|
||||
) {
|
||||
addSetStateCallEntry(value.callee, setStateCallCache);
|
||||
} else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
// Handle values derived from useState calls
|
||||
if (isUseStateType(lvalue.identifier)) {
|
||||
const stateValueSource = value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.add(stateValueSource.identifier.id);
|
||||
}
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
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,
|
||||
]);
|
||||
}
|
||||
} else if (
|
||||
(instr.value.kind === 'CallExpression' ||
|
||||
instr.value.kind === 'MethodCall') &&
|
||||
isUseStateType(instr.lvalue.identifier) &&
|
||||
instr.value.args.length > 0
|
||||
) {
|
||||
const stateValueSource = instr.value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.push({
|
||||
place: stateValueSource,
|
||||
typeOfValue: typeOfValue,
|
||||
sources: [stateValueSource],
|
||||
});
|
||||
}
|
||||
// Validate useEffect calls
|
||||
else if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = functionExpressionsCache.get(
|
||||
value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
validateEffect(
|
||||
effectFunction?.loweredFunc.func,
|
||||
effectSetStateCache,
|
||||
derivationCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const opSource = derivationCache.get(operand.identifier.id);
|
||||
if (opSource === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, opSource.typeOfValue);
|
||||
sources.push(opSource);
|
||||
|
||||
if (
|
||||
(instr.value.kind === 'CallExpression' ||
|
||||
instr.value.kind === 'MethodCall') &&
|
||||
opSource.typeOfValue === 'fromProps' &&
|
||||
isUseStateType(instr.lvalue.identifier)
|
||||
) {
|
||||
opSource.sources.forEach(source => {
|
||||
if (source.identifier.name !== null) {
|
||||
if (shadowingUseState.has(source.identifier.name.value)) {
|
||||
shadowingUseState
|
||||
.get(source.identifier.name.value)
|
||||
?.push(instr.lvalue.loc);
|
||||
} else {
|
||||
shadowingUseState.set(source.identifier.name.value, [
|
||||
instr.lvalue.loc,
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parseOperands(instr, derivationCache, typeOfValue, sources);
|
||||
if (typeOfValue !== 'ignored') {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
updateDerivationMetadata(lvalue, sources, typeOfValue, derivationCache);
|
||||
}
|
||||
|
||||
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,
|
||||
derivationCache,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSetStateCallEntry(
|
||||
callee: Place,
|
||||
setStateCallCache: SetStateCallCache,
|
||||
) {
|
||||
if (callee.loc === GeneratedSource) {
|
||||
return;
|
||||
}
|
||||
function parseBlockPhi(
|
||||
block: BasicBlock,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
for (const phi of block.phis) {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let sources: Array<DerivationMetadata> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
const opSource = derivationCache.get(operand.identifier.id);
|
||||
|
||||
if (setStateCallCache.has(callee.loc.identifierName)) {
|
||||
setStateCallCache.get(callee.loc.identifierName)!.push(callee);
|
||||
} else {
|
||||
setStateCallCache.set(callee.loc.identifierName, [callee]);
|
||||
if (opSource === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, opSource?.typeOfValue ?? 'ignored');
|
||||
sources.push(opSource);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
updateDerivationMetadata(
|
||||
phi.place,
|
||||
sources,
|
||||
typeOfValue,
|
||||
derivationCache,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction | undefined,
|
||||
effectSetStateCache: SetStateCallCache,
|
||||
derivationCache: DerivationCache,
|
||||
stateDerivationErrors: Array<ErrorMetadata>,
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
effectSetStates: Map<string | undefined | null, Array<Place>>,
|
||||
errors: Array<ErrorMetadata>,
|
||||
): void {
|
||||
if (effectFunction === undefined) {
|
||||
let isUsingDerivedDeps = false;
|
||||
for (const dep of effectDeps) {
|
||||
const depMetadata = derivationCache.get(dep);
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) !=
|
||||
null ||
|
||||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
|
||||
) {
|
||||
isUsingDerivedDeps = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUsingDerivedDeps) {
|
||||
// no prop/state derived deps were used in the body of the effect
|
||||
return;
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const effectDerivedSetStateCalls: Array<DerivedSetStateCall> = [];
|
||||
|
||||
const derivedSetStateCall: Array<SetStateCall> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -302,22 +502,68 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const {value} = instr;
|
||||
if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isSetStateType(value.callee.identifier) &&
|
||||
value.args.length === 1 &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
addSetStateCallEntry(value.callee, effectSetStateCache);
|
||||
const argMetadata = derivationCache.get(value.args[0].identifier.id);
|
||||
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: value,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
});
|
||||
parseBlockPhi(block, derivationCache);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
// Early return if any instruction is deriving a value from a ref
|
||||
if (isUseRefType(instr.lvalue.identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
case 'LoadGlobal': {
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad':
|
||||
case 'BinaryExpression':
|
||||
case 'TemplateLiteral':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const derivedDep = derivationCache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (derivedDep !== undefined) {
|
||||
derivedSetStateCall.push({
|
||||
loc: instr.value.callee.loc,
|
||||
setStateId: instr.value.callee.identifier.id,
|
||||
derivedDep: derivedDep,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,160 +571,35 @@ function validateEffect(
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
generateDerivedComputationDetails(
|
||||
effectDerivedSetStateCalls,
|
||||
derivationCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
for (const call of derivedSetStateCall) {
|
||||
const derivedDepsStr = Array.from(call.derivedDep.sources)
|
||||
.map(place => {
|
||||
return place.identifier.name?.value;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
function generateDerivedComputationDetails(
|
||||
effectDerivedSetStateCalls: Array<DerivedSetStateCall>,
|
||||
derivationCache: DerivationCache,
|
||||
stateDerivationErrors: Array<ErrorMetadata>,
|
||||
) {
|
||||
console.log(derivationCache);
|
||||
for (const derivedCall of effectDerivedSetStateCalls) {
|
||||
const arg = derivedCall.value.args[0];
|
||||
if (arg.kind === 'Identifier') {
|
||||
const argMetadata = derivationCache.get(arg.identifier.id);
|
||||
if (argMetadata !== undefined) {
|
||||
const derivationSources: Array<string> = [];
|
||||
let errorDescription = '';
|
||||
|
||||
for (const sourceId of argMetadata.sourcesIds) {
|
||||
const sourceMetadata = derivationCache.get(sourceId);
|
||||
if (sourceMetadata !== undefined) {
|
||||
const sourceName =
|
||||
sourceMetadata.place.identifier.name?.value ||
|
||||
`identifier_${sourceId}`;
|
||||
derivationSources.push(sourceName);
|
||||
}
|
||||
}
|
||||
|
||||
let derivationType: string;
|
||||
switch (argMetadata.typeOfValue) {
|
||||
case 'fromProps':
|
||||
derivationType = 'props';
|
||||
break;
|
||||
case 'fromState':
|
||||
derivationType = 'local state';
|
||||
break;
|
||||
case 'fromPropsAndState':
|
||||
derivationType = 'local state and props';
|
||||
break;
|
||||
default:
|
||||
derivationType = 'unknown source';
|
||||
break;
|
||||
}
|
||||
|
||||
const sourcesList =
|
||||
derivationSources.length > 0
|
||||
? ` [${derivationSources.join(', ')}]`
|
||||
: '';
|
||||
|
||||
const formattedDetails = `State is being derived from ${derivationType}${sourcesList}`;
|
||||
|
||||
stateDerivationErrors.push({
|
||||
derivedComputationDetails: formattedDetails,
|
||||
loc: derivedCall.value.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseOperands(
|
||||
instr: Instruction,
|
||||
derivationCache: DerivationCache,
|
||||
typeOfValue: TypeOfValue,
|
||||
sourceIds: Set<IdentifierId>,
|
||||
) {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const operandMetadata = derivationCache.get(operand.identifier.id);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
if (call.derivedDep.typeOfValue === 'fromProps') {
|
||||
errorDescription = `props [${derivedDepsStr}]`;
|
||||
} else if (call.derivedDep.typeOfValue === 'fromState') {
|
||||
errorDescription = `local state [${derivedDepsStr}]`;
|
||||
} else {
|
||||
errorDescription = `both props and local state [${derivedDepsStr}]`;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
for (const id of operandMetadata.sourcesIds) {
|
||||
sourceIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
return;
|
||||
}
|
||||
|
||||
propagateTypeOfValue(instr, sourceIds, typeOfValue, derivationCache);
|
||||
}
|
||||
|
||||
function propagateTypeOfValue(
|
||||
instr: Instruction,
|
||||
sourceIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
derivationCache: DerivationCache,
|
||||
): void {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
addDerivationEntry(lvalue, sourceIds, typeOfValue, derivationCache);
|
||||
}
|
||||
|
||||
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)) {
|
||||
addDerivationEntry(operand, sourceIds, typeOfValue, derivationCache);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
errors.push({
|
||||
type: call.derivedDep.typeOfValue,
|
||||
description: `${errorDescription}`,
|
||||
loc: call.loc,
|
||||
setStateName:
|
||||
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
|
||||
derivedDepsNames: Array.from(call.derivedDep.sources)
|
||||
.map(place => {
|
||||
return place.identifier.name?.value ?? '';
|
||||
})
|
||||
.filter(Boolean),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function generateCompilerErrors(stateDerivationErrors: Array<ErrorMetadata>) {
|
||||
const throwableErrors = new CompilerError();
|
||||
for (const e of stateDerivationErrors) {
|
||||
throwableErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
description:
|
||||
DERIVE_IN_RENDER_DESCRIPTION + `\n\n${e.derivedComputationDetails}`,
|
||||
category: ErrorCategory.EffectStateDerivationCalculateInRender,
|
||||
reason: DERIVE_IN_RENDER_REASON,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: e.loc,
|
||||
message: DERIVE_IN_RENDER_DETAIL_MESSAGE,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return throwableErrors;
|
||||
}
|
||||
|
||||
@@ -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: [{}],
|
||||
};
|
||||
@@ -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,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,73 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(myRef.current + test);
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState("");
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
setLocal(myRef.current + test);
|
||||
};
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: "testString" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) nulltestString
|
||||
@@ -0,0 +1,19 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(myRef.current + test);
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
@@ -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,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 might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Both props and local state [prefix, name]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.bug-derived-state-from-mixed-deps.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setDisplayName(prefix + name);
|
||||
| ^^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
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,86 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function Component({props, number}) {
|
||||
const nothing = 0;
|
||||
const missDirection = number;
|
||||
const [displayValue, setDisplayValue] = useState(
|
||||
props.prefix + missDirection + nothing
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayValue(props.prefix + missDirection + nothing);
|
||||
}, [props.prefix, missDirection, nothing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
setDisplayValue('clicked');
|
||||
}}>
|
||||
{displayValue}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Local state shadows parent state.
|
||||
|
||||
The setState within a useEffect is deriving from props [props, number]. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render.
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:12:4
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
> 12 | setDisplayValue(props.prefix + missDirection + nothing);
|
||||
| ^^^^^^^^^^^^^^^ this derives values from props to synchronize state
|
||||
13 | }, [props.prefix, missDirection, nothing]);
|
||||
14 |
|
||||
15 | return (
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:7:42
|
||||
5 | const nothing = 0;
|
||||
6 | const missDirection = number;
|
||||
> 7 | const [displayValue, setDisplayValue] = useState(
|
||||
| ^^^^^^^^^
|
||||
> 8 | props.prefix + missDirection + nothing
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | );
|
||||
| ^^^^ this useState shadows props
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
12 | setDisplayValue(props.prefix + missDirection + nothing);
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:7:42
|
||||
5 | const nothing = 0;
|
||||
6 | const missDirection = number;
|
||||
> 7 | const [displayValue, setDisplayValue] = useState(
|
||||
| ^^^^^^^^^
|
||||
> 8 | props.prefix + missDirection + nothing
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | );
|
||||
| ^^^^ this useState shadows number
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
12 | setDisplayValue(props.prefix + missDirection + nothing);
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:18:8
|
||||
16 | <div
|
||||
17 | onClick={() => {
|
||||
> 18 | setDisplayValue('clicked');
|
||||
| ^^^^^^^^^^^^^^^ this setState updates the shadowed state, but should call an onChange event from the parent
|
||||
19 | }}>
|
||||
20 | {displayValue}
|
||||
21 | </div>
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function Component({props, number}) {
|
||||
const nothing = 0;
|
||||
const missDirection = number;
|
||||
const [displayValue, setDisplayValue] = useState(
|
||||
props.prefix + missDirection + nothing
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayValue(props.prefix + missDirection + nothing);
|
||||
}, [props.prefix, missDirection, nothing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
setDisplayValue('clicked');
|
||||
}}>
|
||||
{displayValue}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
## 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}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [value]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-with-conditional.ts:9:6
|
||||
7 | useEffect(() => {
|
||||
8 | if (enabled) {
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | } else {
|
||||
11 | setLocalValue('disabled');
|
||||
12 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -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,47 @@
|
||||
|
||||
## 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'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [value]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-with-side-effects.ts:9:4
|
||||
7 | useEffect(() => {
|
||||
8 | console.log('Value changed:', value);
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | document.title = `Value: ${value}`;
|
||||
11 | }, [value]);
|
||||
12 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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 might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Local state [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
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)
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
## 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: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [props, props, props]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-computed.ts:9:4
|
||||
7 | useEffect(() => {
|
||||
8 | const computed = props.prefix + props.value + props.suffix;
|
||||
> 9 | setDisplayValue(computed);
|
||||
| ^^^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [props.prefix, props.value, props.suffix]);
|
||||
11 |
|
||||
12 | return <div>{displayValue}</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: ']'}],
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [props, props]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-destructured.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(props.firstName + ' ' + props.lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
11 | }, [props.firstName, props.lastName]);
|
||||
12 |
|
||||
13 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.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 might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-in-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
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,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
export default function InProductLobbyGeminiCard(input = 'empty') {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input);
|
||||
}, [input]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [input]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-with-default-value.ts:7:4
|
||||
5 |
|
||||
6 | useEffect(() => {
|
||||
> 7 | setCurrInput(input);
|
||||
| ^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
8 | }, [input]);
|
||||
9 |
|
||||
10 | return <div>{currInput}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
export default function InProductLobbyGeminiCard(input = 'empty') {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input);
|
||||
}, [input]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
@@ -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 might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Local state [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-state-in-effect.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
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,63 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
function EndDate({startDate, endDate, onStartDateChange}) {
|
||||
const [localStartDate, setLocalStartDate] = useState(startDate);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStartDate(startDate);
|
||||
}, [startDate]);
|
||||
|
||||
const onChange = date => {
|
||||
setLocalStartDate(date);
|
||||
onStartDateChange(date);
|
||||
};
|
||||
return (
|
||||
<DateInput value={localStartDate} second={endDate} onChange={onChange} />
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Local state shadows parent state.
|
||||
|
||||
The setState within a useEffect is deriving from props [startDate]. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render.
|
||||
|
||||
error.shadowed-props-with-onchange.ts:7:4
|
||||
5 |
|
||||
6 | useEffect(() => {
|
||||
> 7 | setLocalStartDate(startDate);
|
||||
| ^^^^^^^^^^^^^^^^^ this derives values from props to synchronize state
|
||||
8 | }, [startDate]);
|
||||
9 |
|
||||
10 | const onChange = date => {
|
||||
|
||||
error.shadowed-props-with-onchange.ts:4:46
|
||||
2 |
|
||||
3 | function EndDate({startDate, endDate, onStartDateChange}) {
|
||||
> 4 | const [localStartDate, setLocalStartDate] = useState(startDate);
|
||||
| ^^^^^^^^^^^^^^^^^^^ this useState shadows startDate
|
||||
5 |
|
||||
6 | useEffect(() => {
|
||||
7 | setLocalStartDate(startDate);
|
||||
|
||||
error.shadowed-props-with-onchange.ts:11:4
|
||||
9 |
|
||||
10 | const onChange = date => {
|
||||
> 11 | setLocalStartDate(date);
|
||||
| ^^^^^^^^^^^^^^^^^ this setState updates the shadowed state, but should call an onChange event from the parent
|
||||
12 | onStartDateChange(date);
|
||||
13 | };
|
||||
14 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
function EndDate({startDate, endDate, onStartDateChange}) {
|
||||
const [localStartDate, setLocalStartDate] = useState(startDate);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStartDate(startDate);
|
||||
}, [startDate]);
|
||||
|
||||
const onChange = date => {
|
||||
setLocalStartDate(date);
|
||||
onStartDateChange(date);
|
||||
};
|
||||
return (
|
||||
<DateInput value={localStartDate} second={endDate} onChange={onChange} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (myRef.current) {
|
||||
setLocal(test + 'Available');
|
||||
} else {
|
||||
setLocal(test + 'NotAvailable');
|
||||
}
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState("");
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
if (myRef.current) {
|
||||
setLocal(test + "Available");
|
||||
} else {
|
||||
setLocal(test + "NotAvailable");
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: "testString" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) testStringNotAvailable
|
||||
@@ -0,0 +1,23 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (myRef.current) {
|
||||
setLocal(test + 'Available');
|
||||
} else {
|
||||
setLocal(test + 'NotAvailable');
|
||||
}
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
@@ -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: [],
|
||||
};
|
||||
@@ -10494,16 +10494,7 @@ string-length@^4.0.1:
|
||||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -10576,14 +10567,7 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -11360,7 +11344,7 @@ workerpool@^6.5.1:
|
||||
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
|
||||
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -11378,15 +11362,6 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||
|
||||
@@ -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 () =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user