Compare commits

..

5 Commits

Author SHA1 Message Date
Jorge Cabiedes
853550e7c8 [compiler] Added check for if the same invalid setSate within an effect is used elsewhere 2025-09-09 14:11:24 -07:00
Jorge Cabiedes
5cf71b322d [compiler] Validation for values derived from props in useEffect ready 2025-09-09 14:11:22 -07:00
Jorge Cabiedes
f807ce6492 [compiler] Basic solution for instruction based prop derivation validation 2025-09-09 14:11:19 -07:00
Lauren Tan
7b38acca0b [compiler][wip] Extend ValidateNoDerivedComputationsInEffects for props derived effects
This PR adds infra to disambiguate between two types of derived state in effects:
  1. State derived from props
  2. State derived from other state

TODO:
- [ ] Props tracking through destructuring and property access does not seem to be propagated correctly inside of Functions' instructions (or i might be misunderstanding how we track aliasing effects)
- [ ] compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.js should be failing
- [ ] Handle "mixed" case where deps flow from at least one prop AND state. Should probably have a different error reason, to aid with categorization
2025-09-09 14:10:44 -07:00
Lauren Tan
1d9c3927ea [compiler] new tests for props derived
Adds some new test cases for ValidateNoDerivedComputationsInEffects.
2025-09-09 14:09:57 -07:00
169 changed files with 2979 additions and 4610 deletions

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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:

View File

@@ -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
View File

@@ -23,7 +23,6 @@ chrome-user-data
.vscode
*.swp
*.swo
/tmp
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
</>

View File

@@ -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>
);
}

View File

@@ -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}
/>
</>
);
}

View File

@@ -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>

View File

@@ -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>
);
});

View File

@@ -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;

View File

@@ -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>
);
}

View 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;
}
}

View File

@@ -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 = {

View File

@@ -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',

View File

@@ -520,7 +520,7 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
case ErrorCategory.AutomaticEffectDependencies:
case ErrorCategory.CapitalizedCalls:
case ErrorCategory.Config:
case ErrorCategory.EffectStateDerivationCalculateInRender:
case ErrorCategory.EffectDerivationsOfState:
case ErrorCategory.EffectSetState:
case ErrorCategory.ErrorBoundaries:
case ErrorCategory.Factories:
@@ -614,10 +614,7 @@ export enum ErrorCategory {
* Checks for no setState in effect bodies
*/
EffectSetState = 'EffectSetState',
/**
* Checks for no deriving state in effects, solved by calculate in render
*/
EffectStateDerivationCalculateInRender = 'EffectStateDerivationCalculateInRender',
EffectDerivationsOfState = 'EffectDerivationsOfState',
/**
* Validates against try/catch in place of error boundaries
*/
@@ -754,11 +751,11 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
recommended: false,
};
}
case ErrorCategory.EffectStateDerivationCalculateInRender: {
case ErrorCategory.EffectDerivationsOfState: {
return {
category,
severity: ErrorSeverity.Error,
name: 'no-deriving-state-in-effects-calculate-in-render',
name: 'no-deriving-state-in-effects',
description:
'Validates against deriving values from state in an effect',
recommended: false,

View File

@@ -276,7 +276,7 @@ function runWithEnvironment(
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir, env));
env.logErrors(validateNoSetStateInEffects(hir));
}
if (env.config.validateNoJSXInTryStatements) {

View File

@@ -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;
}

View File

@@ -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>;

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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,
});

View File

@@ -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);
}

View File

@@ -5,149 +5,60 @@
* LICENSE file in the root directory of this source tree.
*/
import {effect} from 'zod';
import {CompilerError, Effect, ErrorSeverity, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BasicBlock,
GeneratedSource,
BlockId,
Identifier,
FunctionExpression,
HIRFunction,
IdentifierId,
Instruction,
isSetStateType,
Place,
isUseStateType,
Effect,
isSetStateType,
isUseEffectHookType,
FunctionExpression,
BlockId,
SourceLocation,
CallExpression,
isUseStateType,
IdentifierName,
GeneratedSource,
} from '../HIR';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {printInstruction} from '../HIR/PrintHIR';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
} from '../CompilerError';
eachInstructionOperand,
eachTerminalOperand,
eachInstructionLValue,
eachPatternOperand,
} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {assertExhaustive} from '../Utils/utils';
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
type SetStateCall = {
loc: SourceLocation;
invalidDeps: Map<Identifier, Place[]> | undefined;
setStateId: IdentifierId;
};
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
type SetStateName = string | undefined | null;
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>;
// TODO: Rename to place
identifierPlace: Place;
sources: Place[];
};
// TODO: This needs refining
type ErrorMetadata = {
derivedComputationDetails: string;
errorType: 'HoistState' | 'CalculateInRender';
propInfo: string | undefined;
localStateInfo: string | undefined;
loc: SourceLocation;
setStateName: SetStateName;
};
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
*/
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 stateDerivationErrors: Array<ErrorMetadata> = [];
parseFNParameters(fn, derivationCache);
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]),
typeOfValue: 'fromProps',
});
}
}
} else if (fn.fnType === 'Component') {
const props = fn.params[0];
if (props != null && props.kind === 'Identifier') {
derivationCache.set(props.identifier.id, {
place: props,
sourcesIds: new Set([props.identifier.id]),
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);
if (operandMetadata === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
sourcesIds.add(operand.identifier.id);
}
if (typeOfValue !== 'ignored') {
addDerivationEntry(phi.place, sourcesIds, typeOfValue, derivationCache);
}
}
}
function joinValue(
lvalueType: TypeOfValue,
valueType: TypeOfValue,
@@ -155,146 +66,392 @@ 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>,
function updateDerivationMetadata(
target: Place,
sources: DerivationMetadata[],
typeOfValue: TypeOfValue,
derivationCache: DerivationCache,
derivedTuple: Map<IdentifierId, DerivationMetadata>,
): void {
let newValue: DerivationMetadata = {
place: derivedVar,
sourcesIds: new Set(),
typeOfValue: typeOfValue ?? 'ignored',
identifierPlace: target,
sources: [],
typeOfValue: typeOfValue,
};
if (sourcesIds !== undefined) {
for (const id of sourcesIds) {
const sourcePlace = derivationCache.get(id)?.place;
if (sourcePlace === undefined) {
continue;
}
/*
* 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 source of sources) {
// If the identifier of the source is a promoted identifier, then
// we should set the target as the source.
if (source.identifierPlace.identifier.name?.kind === 'promoted') {
newValue.sources.push(target);
} else {
newValue.sources.push(...source.sources);
}
}
derivationCache.set(derivedVar.identifier.id, newValue);
derivedTuple.set(target.identifier.id, newValue);
}
function parseInstr(
instr: Instruction,
derivationCache: DerivationCache,
setStateCallCache: SetStateCallCache,
effectSetStateCache: SetStateCallCache,
functionExpressionsCache: FunctionExpressionsCache,
stateDerivationErrors: Array<ErrorMetadata>,
): void {
const {value, lvalue} = instr;
derivedTuple: Map<IdentifierId, DerivationMetadata>,
setStateCalls: Map<SetStateName, Place[]>,
) {
// console.log(printInstruction(instr));
// console.log(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) {
for (const instr of block.instructions) {
functionExpressionsCache.set(lvalue.identifier.id, value);
parseInstr(
instr,
derivationCache,
setStateCallCache,
effectSetStateCache,
functionExpressionsCache,
stateDerivationErrors,
);
}
}
}
// Record setState calls
else if (
value.kind === 'CallExpression' &&
isSetStateType(value.callee.identifier)
// If the instruction is destructuring a useState hook call
if (
instr.value.kind === 'Destructure' &&
instr.value.lvalue.pattern.kind === 'ArrayPattern' &&
isUseStateType(instr.value.value.identifier)
) {
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');
}
// 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,
);
const value = instr.value.lvalue.pattern.items[0];
if (value.kind === 'Identifier') {
derivedTuple.set(value.identifier.id, {
identifierPlace: value,
sources: [value],
typeOfValue: 'fromState',
});
}
}
parseOperands(instr, derivationCache, typeOfValue, sources);
// If the instruction is calling a setState
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier' &&
instr.value.callee.loc !== GeneratedSource
) {
if (setStateCalls.has(instr.value.callee.loc.identifierName)) {
setStateCalls
.get(instr.value.callee.loc.identifierName)!
.push(instr.value.callee);
} else {
setStateCalls.set(instr.value.callee.loc.identifierName, [
instr.value.callee,
]);
}
}
let sources: DerivationMetadata[] = [];
for (const operand of eachInstructionOperand(instr)) {
const opSource = derivedTuple.get(operand.identifier.id);
if (opSource === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, opSource.typeOfValue);
sources.push(opSource);
}
if (typeOfValue !== 'ignored') {
for (const lvalue of eachInstructionLValue(instr)) {
updateDerivationMetadata(lvalue, sources, typeOfValue, derivedTuple);
}
for (const operand of eachInstructionOperand(instr)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
updateDerivationMetadata(
operand,
sources,
typeOfValue,
derivedTuple,
);
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
loc: operand.loc,
suggestions: null,
});
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
}
function addSetStateCallEntry(
callee: Place,
setStateCallCache: SetStateCallCache,
function parseBlockPhi(
block: BasicBlock,
derivedTuple: Map<IdentifierId, DerivationMetadata>,
) {
if (callee.loc === GeneratedSource) {
return;
for (const phi of block.phis) {
for (const operand of phi.operands.values()) {
const source = derivedTuple.get(operand.identifier.id);
if (source !== undefined && source.typeOfValue === 'fromProps') {
if (
source.identifierPlace.identifier.name === null ||
source.identifierPlace.identifier.name?.kind === 'promoted'
) {
derivedTuple.set(phi.place.identifier.id, {
identifierPlace: phi.place,
sources: [phi.place],
typeOfValue: 'fromProps',
});
} else {
derivedTuple.set(phi.place.identifier.id, {
identifierPlace: phi.place,
sources: source.sources,
typeOfValue: 'fromProps',
});
}
}
}
}
}
/**
* 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 candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const derivedTuple: Map<IdentifierId, DerivationMetadata> = new Map();
const effectSetStates: Map<SetStateName, Place[]> = new Map();
const setStateCalls: Map<SetStateName, Place[]> = new Map();
const errors: ErrorMetadata[] = [];
if (fn.fnType === 'Hook') {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
derivedTuple.set(param.identifier.id, {
identifierPlace: param,
sources: [param],
typeOfValue: 'fromProps',
});
}
}
} else if (fn.fnType === 'Component') {
const props = fn.params[0];
if (props != null && props.kind === 'Identifier') {
derivedTuple.set(props.identifier.id, {
identifierPlace: props,
sources: [props],
typeOfValue: 'fromProps',
});
}
}
if (setStateCallCache.has(callee.loc.identifierName)) {
setStateCallCache.get(callee.loc.identifierName)!.push(callee);
} else {
setStateCallCache.set(callee.loc.identifierName, [callee]);
for (const block of fn.body.blocks.values()) {
parseBlockPhi(block, derivedTuple);
for (const instr of block.instructions) {
const {lvalue, value} = instr;
parseInstr(instr, derivedTuple, setStateCalls);
/*
* Special case for function expressions, we need to parse nested instructions
* TODO: Can there be more recursive levels?
*/
if (value.kind === 'FunctionExpression') {
for (const [, block] of value.loweredFunc.func.body.blocks) {
for (const instr of block.instructions) {
parseInstr(instr, derivedTuple, setStateCalls);
}
}
}
// Maybe this should run for every instruction being parsed
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,
derivedTuple,
effectSetStates,
errors,
);
}
}
}
}
}
const throwableErrors = new CompilerError();
for (const error of errors) {
let reason;
let description = '';
// TODO: Not sure if this is robust enough.
/*
* If we use a setState from an invalid useEffect elsewhere then we probably have to
* hoist state up, else we should calculate in render
*/
if (
setStateCalls.get(error.setStateName)?.length !=
effectSetStates.get(error.setStateName)?.length
) {
reason =
'Consider lifting state up to the parent component to make this a controlled component. (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)';
} else {
reason =
'You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)';
}
if (error.propInfo !== undefined) {
description += error.propInfo;
}
if (error.localStateInfo !== undefined) {
description += error.localStateInfo;
}
throwableErrors.push({
reason: reason,
description: description,
severity: ErrorSeverity.InvalidReact,
loc: error.loc,
});
}
if (throwableErrors.hasAnyErrors()) {
throw throwableErrors;
}
}
function validateEffect(
effectFunction: HIRFunction | undefined,
effectSetStateCache: SetStateCallCache,
derivationCache: DerivationCache,
stateDerivationErrors: Array<ErrorMetadata>,
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
derivedTuple: Map<IdentifierId, DerivationMetadata>,
effectSetStates: Map<SetStateName, Place[]>,
errors: ErrorMetadata[],
): void {
if (effectFunction === undefined) {
/*
* TODO: This makes it so we only capture single line useEffects.
* We should be able to capture multiline as well
*/
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else if (derivedTuple.has(operand.identifier.id)) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
}
}
// This might be wrong gotta double check
let hasInvalidDep = false;
for (const dep of effectDeps) {
const depMetadata = derivedTuple.get(dep);
if (
effectFunction.context.find(operand => operand.identifier.id === dep) !=
null ||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
) {
hasInvalidDep = true;
}
}
if (!hasInvalidDep) {
console.log('early return 2');
// effect dep wasn't actually used in the function
return;
}
const seenBlocks: Set<BlockId> = new Set();
const effectDerivedSetStateCalls: Array<DerivedSetStateCall> = [];
// This variable is suspicious maybe we don't need it?
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
const effectInvalidlyDerived: Map<IdentifierId, DerivationMetadata> =
new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
const depMetadata = derivedTuple.get(dep);
if (depMetadata !== undefined) {
effectInvalidlyDerived.set(dep, depMetadata);
}
}
const setStateCallsInEffect: Array<SetStateCall> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
@@ -302,183 +459,133 @@ 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, effectInvalidlyDerived);
for (const instr of block.instructions) {
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier' &&
instr.value.callee.loc !== GeneratedSource &&
instr.value.callee.loc.identifierName !== undefined &&
instr.value.callee.loc.identifierName !== null
) {
if (effectSetStates.has(instr.value.callee.loc.identifierName)) {
effectSetStates
.get(instr.value.callee.loc.identifierName)!
.push(instr.value.callee);
} else {
effectSetStates.set(instr.value.callee.loc.identifierName, [
instr.value.callee,
]);
}
}
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
case 'LoadGlobal': {
break;
}
case 'LoadLocal': {
const deps = values.get(instr.value.place.identifier.id);
if (deps != null) {
values.set(instr.lvalue.identifier.id, deps);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad':
case 'BinaryExpression':
case 'TemplateLiteral':
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionOperand(instr)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const propSources = derivedTuple.get(
instr.value.args[0].identifier.id,
);
if (propSources !== undefined) {
setStateCallsInEffect.push({
loc: instr.value.callee.loc,
setStateId: instr.value.callee.identifier.id,
invalidDeps: new Map([
[instr.value.args[0].identifier, propSources.sources],
]),
});
} else {
setStateCallsInEffect.push({
loc: instr.value.callee.loc,
setStateId: instr.value.callee.identifier.id,
invalidDeps: undefined,
});
}
}
break;
}
default: {
console.log('early return 4');
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
return;
}
}
seenBlocks.add(block.id);
}
generateDerivedComputationDetails(
effectDerivedSetStateCalls,
derivationCache,
stateDerivationErrors,
);
}
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> = [];
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,
});
for (const call of setStateCallsInEffect) {
if (call.invalidDeps != null) {
let propNames = '';
for (const [, places] of call.invalidDeps.entries()) {
const placeNames = places
.map(place => place.identifier.name?.value)
.join(', ');
propNames += `[${placeNames}], `;
}
propNames = propNames.slice(0, -2);
const propInfo = propNames ? ` (from props '${propNames}')` : '';
errors.push({
errorType: 'HoistState',
propInfo: propInfo,
localStateInfo: undefined,
loc: call.loc,
setStateName:
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
});
} else {
errors.push({
errorType: 'CalculateInRender',
propInfo: undefined,
localStateInfo: undefined,
loc: call.loc,
setStateName:
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
});
}
}
}
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;
}
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}\``,
);
}
}
}
}
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;
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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)

View File

@@ -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: [{}],
};

View File

@@ -24,13 +24,15 @@ function BadExample() {
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;

View File

@@ -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 = {
```

View File

@@ -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: [{}],
};

View File

@@ -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 = ... }`
```

View File

@@ -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: [{}],
};

View File

@@ -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 | }
```

View File

@@ -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);
}

View File

@@ -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 | }
```

View File

@@ -1,9 +0,0 @@
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
function Component({setX}) {
const aliased = setX;
setX(1);
aliased(2);
return x;
}

View File

@@ -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>

View File

@@ -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: [{}],
};

View File

@@ -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>

View File

@@ -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'}],
};

View File

@@ -0,0 +1,79 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { value, enabled } = t0;
const [localValue, setLocalValue] = useState("");
let t1;
let t2;
if ($[0] !== enabled || $[1] !== value) {
t1 = () => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue("disabled");
}
};
t2 = [value, enabled];
$[0] = enabled;
$[1] = value;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] !== localValue) {
t3 = <div>{localValue}</div>;
$[4] = localValue;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: "test", enabled: true }],
};
```
### Eval output
(kind: ok) <div>test</div>

View File

@@ -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}],
};

View File

@@ -0,0 +1,74 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
console.log('Value changed:', value);
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(5);
const { value } = t0;
const [localValue, setLocalValue] = useState("");
let t1;
let t2;
if ($[0] !== value) {
t1 = () => {
console.log("Value changed:", value);
setLocalValue(value);
document.title = `Value: ${value}`;
};
t2 = [value];
$[0] = value;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== localValue) {
t3 = <div>{localValue}</div>;
$[3] = localValue;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: "test" }],
};
```
### Eval output
(kind: ok) <div>test</div>
logs: ['Value changed:','test']

View File

@@ -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'}],
};

View File

@@ -0,0 +1,51 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({prefix}) {
const [name, setName] = useState('');
const [displayName, setDisplayName] = useState('');
useEffect(() => {
setDisplayName(prefix + name);
}, [prefix, name]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<div>{displayName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: 'Hello, '}],
};
```
## Error
```
Found 1 error:
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
error.bug-derived-state-from-mixed-deps.ts:9:4
7 |
8 | useEffect(() => {
> 9 | setDisplayName(prefix + name);
| ^^^^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [prefix, name]);
11 |
12 | return (
```

View File

@@ -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, '}],
};

View File

@@ -0,0 +1,45 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({user: {firstName, lastName}}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{user: {firstName: 'John', lastName: 'Doe'}}],
};
```
## Error
```
Found 1 error:
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
error.invalid-derived-state-from-props-destructured.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
9 | }, [firstName, lastName]);
10 |
11 | return <div>{fullName}</div>;
```

View File

@@ -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'}],
};

View File

@@ -0,0 +1,45 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({firstName, lastName}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John', lastName: 'Doe'}],
};
```
## Error
```
Found 1 error:
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
error.invalid-derived-state-from-props-in-effect.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
9 | }, [firstName, lastName]);
10 |
11 | return <div>{fullName}</div>;
```

View File

@@ -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'}],
};

View File

@@ -0,0 +1,53 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return (
<div>
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Error
```
Found 1 error:
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
error.invalid-derived-state-from-state-in-effect.ts:10:4
8 |
9 | useEffect(() => {
> 10 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
11 | }, [firstName, lastName]);
12 |
13 | return (
```

View File

@@ -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: [],
};

View File

@@ -0,0 +1,72 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
import { useEffect, useState } from "react";
function Component(props) {
const $ = _c(7);
const [displayValue, setDisplayValue] = useState("");
let t0;
let t1;
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
t0 = () => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
};
t1 = [props.prefix, props.value, props.suffix];
$[0] = props.prefix;
$[1] = props.suffix;
$[2] = props.value;
$[3] = t0;
$[4] = t1;
} else {
t0 = $[3];
t1 = $[4];
}
useEffect(t0, t1);
let t2;
if ($[5] !== displayValue) {
t2 = <div>{displayValue}</div>;
$[5] = displayValue;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prefix: "[", value: "test", suffix: "]" }],
};
```
### Eval output
(kind: ok) <div>[test]</div>

View File

@@ -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: ']'}],
};

View File

@@ -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')

View File

@@ -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: [],
};

View File

@@ -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

View File

@@ -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: [],
};

View File

@@ -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

View File

@@ -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: [],
};

View File

@@ -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

View File

@@ -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: [],
};

View File

@@ -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')

View File

@@ -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: [],
};

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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]',
},

View File

@@ -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) {

View File

@@ -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": {

View File

@@ -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);

View File

@@ -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) => {

View 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',
),
},
},
],
},
};

View File

@@ -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]',
},
},

View File

@@ -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]',
},

View File

@@ -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;

View File

@@ -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]',
},

View File

@@ -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",

View File

@@ -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.

View File

@@ -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,
},
],

View File

@@ -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>
});

View File

@@ -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]`);
});
});

View File

@@ -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 () =>

View File

@@ -16,35 +16,6 @@ describe('StoreStress (Legacy Mode)', () => {
let store;
let print;
function readValue(promise) {
if (typeof React.use === 'function') {
return React.use(promise);
}
// Support for React < 19.0
switch (promise.status) {
case 'fulfilled':
return promise.value;
case 'rejected':
throw promise.reason;
case 'pending':
throw promise;
default:
promise.status = 'pending';
promise.then(
value => {
promise.status = 'fulfilled';
promise.value = value;
},
reason => {
promise.status = 'rejected';
promise.reason = reason;
},
);
throw promise;
}
}
beforeEach(() => {
bridge = global.bridge;
store = global.store;
@@ -444,116 +415,118 @@ describe('StoreStress (Legacy Mode)', () => {
a,
];
// Excluding Suspense tree here due to different measurement semantics for fallbacks
const stepsSnapshot = [
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<C key="c">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<B key="b">
<A key="a">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
<Suspense>
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<B key="b">
<Y>"
<Y>
`,
`
"[root]
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>"
<Y>
`,
];
const never = new Promise(() => {});
const Never = () => {
readValue(never);
if (React.use) {
React.use(new Promise(() => {}));
} else {
throw new Promise(() => {});
}
};
const Root = ({children}) => {
@@ -576,10 +549,8 @@ describe('StoreStress (Legacy Mode)', () => {
),
);
// We snapshot each step once so it doesn't regress.
expect(print(store, undefined, undefined, false)).toMatchInlineSnapshot(
stepsSnapshot[i],
);
snapshots.push(print(store, undefined, undefined, false));
expect(store).toMatchInlineSnapshot(stepsSnapshot[i]);
snapshots.push(print(store));
act(() => unmount());
expect(print(store)).toBe('');
}
@@ -601,7 +572,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
act(() => unmount());
expect(print(store)).toBe('');
}
@@ -621,7 +592,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
render(
@@ -633,7 +604,7 @@ describe('StoreStress (Legacy Mode)', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
render(
@@ -644,7 +615,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => unmount());
expect(print(store)).toBe('');
@@ -670,7 +641,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
render(
@@ -686,7 +657,7 @@ describe('StoreStress (Legacy Mode)', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
render(
@@ -701,7 +672,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => unmount());
expect(print(store)).toBe('');
@@ -723,7 +694,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
render(
@@ -739,7 +710,7 @@ describe('StoreStress (Legacy Mode)', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
render(
@@ -750,7 +721,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => unmount());
expect(print(store)).toBe('');
@@ -776,7 +747,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
render(
@@ -788,7 +759,7 @@ describe('StoreStress (Legacy Mode)', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
render(
@@ -803,7 +774,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => unmount());
expect(print(store)).toBe('');
@@ -832,7 +803,7 @@ describe('StoreStress (Legacy Mode)', () => {
const suspenseID = store.getElementIDAtIndex(2);
// Force fallback.
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
act(() => {
bridge.send('overrideSuspense', {
id: suspenseID,
@@ -840,7 +811,7 @@ describe('StoreStress (Legacy Mode)', () => {
forceFallback: true,
});
});
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
expect(print(store)).toEqual(snapshots[j]);
// Stop forcing fallback.
act(() => {
@@ -850,7 +821,7 @@ describe('StoreStress (Legacy Mode)', () => {
forceFallback: false,
});
});
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Trigger actual fallback.
act(() =>
@@ -866,7 +837,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
expect(print(store)).toEqual(snapshots[j]);
// Force fallback while we're in fallback mode.
act(() => {
@@ -877,7 +848,7 @@ describe('StoreStress (Legacy Mode)', () => {
});
});
// Keep seeing fallback content.
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
expect(print(store)).toEqual(snapshots[j]);
// Switch to primary mode.
act(() =>
@@ -890,7 +861,7 @@ describe('StoreStress (Legacy Mode)', () => {
),
);
// Fallback is still forced though.
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
expect(print(store)).toEqual(snapshots[j]);
// Stop forcing fallback. This reverts to primary content.
act(() => {
@@ -901,7 +872,7 @@ describe('StoreStress (Legacy Mode)', () => {
});
});
// Now we see primary content.
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => unmount());
@@ -950,8 +921,6 @@ describe('StoreStress (Legacy Mode)', () => {
<A key="a">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -962,8 +931,6 @@ describe('StoreStress (Legacy Mode)', () => {
<A key="a">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -976,8 +943,6 @@ describe('StoreStress (Legacy Mode)', () => {
<C key="c">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -990,8 +955,6 @@ describe('StoreStress (Legacy Mode)', () => {
<A key="a">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -1003,8 +966,6 @@ describe('StoreStress (Legacy Mode)', () => {
<A key="a">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -1016,8 +977,6 @@ describe('StoreStress (Legacy Mode)', () => {
<A key="a">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -1029,8 +988,6 @@ describe('StoreStress (Legacy Mode)', () => {
<A key="a">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -1042,8 +999,6 @@ describe('StoreStress (Legacy Mode)', () => {
<B key="b">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -1054,8 +1009,6 @@ describe('StoreStress (Legacy Mode)', () => {
<A key="a">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -1065,8 +1018,6 @@ describe('StoreStress (Legacy Mode)', () => {
▾ <MaybeSuspend>
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -1077,8 +1028,6 @@ describe('StoreStress (Legacy Mode)', () => {
<B key="b">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
`
[root]
@@ -1089,8 +1038,6 @@ describe('StoreStress (Legacy Mode)', () => {
<A key="a">
<Z>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`,
];
@@ -1102,8 +1049,6 @@ describe('StoreStress (Legacy Mode)', () => {
▾ <Suspense>
<A key="a">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1112,8 +1057,6 @@ describe('StoreStress (Legacy Mode)', () => {
▾ <Suspense>
<A key="a">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1124,8 +1067,6 @@ describe('StoreStress (Legacy Mode)', () => {
<B key="b">
<C key="c">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1136,8 +1077,6 @@ describe('StoreStress (Legacy Mode)', () => {
<B key="b">
<A key="a">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1147,8 +1086,6 @@ describe('StoreStress (Legacy Mode)', () => {
<C key="c">
<A key="a">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1158,8 +1095,6 @@ describe('StoreStress (Legacy Mode)', () => {
<C key="c">
<A key="a">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1169,8 +1104,6 @@ describe('StoreStress (Legacy Mode)', () => {
<C key="c">
<A key="a">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1180,8 +1113,6 @@ describe('StoreStress (Legacy Mode)', () => {
<A key="a">
<B key="b">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1190,8 +1121,6 @@ describe('StoreStress (Legacy Mode)', () => {
▾ <Suspense>
<A key="a">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1199,8 +1128,6 @@ describe('StoreStress (Legacy Mode)', () => {
<X>
<Suspense>
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1209,8 +1136,6 @@ describe('StoreStress (Legacy Mode)', () => {
▾ <Suspense>
<B key="b">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
`
[root]
@@ -1219,14 +1144,15 @@ describe('StoreStress (Legacy Mode)', () => {
▾ <Suspense>
<A key="a">
<Y>
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={null}>
`,
];
const never = new Promise(() => {});
const Never = () => {
readValue(never);
if (React.use) {
React.use(new Promise(() => {}));
} else {
throw new Promise(() => {});
}
};
const MaybeSuspend = ({children, suspend}) => {
@@ -1298,7 +1224,7 @@ describe('StoreStress (Legacy Mode)', () => {
);
// We snapshot each step once so it doesn't regress.
expect(store).toMatchInlineSnapshot(stepsSnapshotTwo[i]);
fallbackSnapshots.push(print(store, undefined, undefined, false));
fallbackSnapshots.push(print(store));
act(() => unmount());
expect(print(store)).toBe('');
}
@@ -1376,9 +1302,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[i],
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
render(
@@ -1397,9 +1321,7 @@ describe('StoreStress (Legacy Mode)', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[j],
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
render(
@@ -1417,9 +1339,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[i],
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
act(() => unmount());
expect(print(store)).toBe('');
@@ -1457,9 +1377,7 @@ describe('StoreStress (Legacy Mode)', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[j],
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
render(
@@ -1496,9 +1414,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[i],
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
render(
@@ -1525,9 +1441,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[i],
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
act(() => unmount());
expect(print(store)).toBe('');
@@ -1566,9 +1480,7 @@ describe('StoreStress (Legacy Mode)', () => {
forceFallback: true,
});
});
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[j],
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback.
act(() => {
@@ -1592,9 +1504,7 @@ describe('StoreStress (Legacy Mode)', () => {
</Root>,
),
);
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[j],
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Force fallback while we're in fallback mode.
act(() => {
@@ -1605,9 +1515,7 @@ describe('StoreStress (Legacy Mode)', () => {
});
});
// Keep seeing fallback content.
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[j],
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Switch to primary mode.
act(() =>
@@ -1622,9 +1530,7 @@ describe('StoreStress (Legacy Mode)', () => {
),
);
// Fallback is still forced though.
expect(print(store, undefined, undefined, false)).toEqual(
fallbackSnapshots[j],
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback. This reverts to primary content.
act(() => {

View File

@@ -88,7 +88,6 @@ import {
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
SUSPENSE_TREE_OPERATION_SUSPENDERS,
UNKNOWN_SUSPENDERS_NONE,
UNKNOWN_SUSPENDERS_REASON_PRODUCTION,
UNKNOWN_SUSPENDERS_REASON_OLD_VERSION,
@@ -1516,7 +1515,7 @@ export function attach(
currentRoot = rootInstance;
unmountInstanceRecursively(rootInstance);
rootToFiberInstanceMap.delete(root);
flushPendingEvents();
flushPendingEvents(root);
currentRoot = (null: any);
});
@@ -1541,7 +1540,7 @@ export function attach(
currentRoot = newRoot;
setRootPseudoKey(currentRoot.id, root.current);
mountFiberRecursively(root.current, false);
flushPendingEvents();
flushPendingEvents(root);
currentRoot = (null: any);
});
@@ -2017,7 +2016,6 @@ export function attach(
const pendingOperations: OperationsArray = [];
const pendingRealUnmountedIDs: Array<FiberInstance['id']> = [];
const pendingRealUnmountedSuspenseIDs: Array<FiberInstance['id']> = [];
const pendingSuspenderChanges: Set<FiberInstance['id']> = new Set();
let pendingOperationsQueue: Array<OperationsArray> | null = [];
const pendingStringTable: Map<string, StringTableEntry> = new Map();
let pendingStringTableLength: number = 0;
@@ -2049,7 +2047,6 @@ export function attach(
pendingOperations.length === 0 &&
pendingRealUnmountedIDs.length === 0 &&
pendingRealUnmountedSuspenseIDs.length === 0 &&
pendingSuspenderChanges.size === 0 &&
pendingUnmountedRootID === null
);
}
@@ -2099,7 +2096,7 @@ export function attach(
}
}
function flushPendingEvents(): void {
function flushPendingEvents(root: Object): void {
if (shouldBailoutWithPendingOperations()) {
// If we aren't profiling, we can just bail out here.
// No use sending an empty update over the bridge.
@@ -2116,7 +2113,6 @@ export function attach(
pendingRealUnmountedIDs.length +
(pendingUnmountedRootID === null ? 0 : 1);
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
const numSuspenderChanges = pendingSuspenderChanges.size;
const operations = new Array<number>(
// Identify which renderer this update is coming from.
@@ -2132,10 +2128,7 @@ export function attach(
// [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
(numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
// Regular operations
pendingOperations.length +
// All suspender changes are batched in a single message.
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders]]
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 2 : 0),
pendingOperations.length,
);
// Identify which renderer this update is coming from.
@@ -2198,31 +2191,12 @@ export function attach(
i++;
}
}
// Fill in pending operations.
// Fill in the rest of the operations.
for (let j = 0; j < pendingOperations.length; j++) {
operations[i + j] = pendingOperations[j];
}
i += pendingOperations.length;
// Suspender changes might affect newly mounted nodes that we already recorded
// in pending operations.
if (numSuspenderChanges > 0) {
operations[i++] = SUSPENSE_TREE_OPERATION_SUSPENDERS;
operations[i++] = numSuspenderChanges;
pendingSuspenderChanges.forEach(fiberIdWithChanges => {
const suspense = idToSuspenseNodeMap.get(fiberIdWithChanges);
if (suspense === undefined) {
// Probably forgot to cleanup pendingSuspenderChanges when this node was removed.
throw new Error(
`Could not send suspender changes for "${fiberIdWithChanges}" since the Fiber no longer exists.`,
);
}
operations[i++] = fiberIdWithChanges;
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
});
}
// Let the frontend know about tree operations.
flushOrQueueOperations(operations);
@@ -2230,7 +2204,6 @@ export function attach(
pendingOperations.length = 0;
pendingRealUnmountedIDs.length = 0;
pendingRealUnmountedSuspenseIDs.length = 0;
pendingSuspenderChanges.clear();
pendingUnmountedRootID = null;
pendingStringTable.clear();
pendingStringTableLength = 0;
@@ -2715,19 +2688,6 @@ export function attach(
}
}
function recordSuspenseSuspenders(suspenseNode: SuspenseNode): void {
if (__DEBUG__) {
console.log('recordSuspenseSuspenders()', suspenseNode);
}
const fiberInstance = suspenseNode.instance;
if (fiberInstance.kind !== FIBER_INSTANCE) {
// TODO: Suspender updates of filtered Suspense nodes are currently dropped.
return;
}
pendingSuspenderChanges.add(fiberInstance.id);
}
function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void {
if (__DEBUG__) {
console.log(
@@ -2749,7 +2709,6 @@ export function attach(
// and later arrange them in the correct order.
pendingRealUnmountedSuspenseIDs.push(id);
pendingSuspenderChanges.delete(id);
idToSuspenseNodeMap.delete(id);
}
@@ -2820,7 +2779,6 @@ export function attach(
) {
// This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
parentSuspenseNode.hasUniqueSuspenders = true;
recordSuspenseSuspenders(parentSuspenseNode);
}
}
// We have observed at least one known reason this might have been suspended.
@@ -2862,9 +2820,6 @@ export function attach(
// We have found a child boundary that depended on the unblocked I/O.
// It can now be marked as having unique suspenders. We can skip its children
// since they'll still be blocked by this one.
if (!node.hasUniqueSuspenders) {
recordSuspenseSuspenders(node);
}
node.hasUniqueSuspenders = true;
node.hasUnknownSuspenders = false;
} else if (node.firstChild !== null) {
@@ -2902,32 +2857,16 @@ export function attach(
// Let's remove it from the parent SuspenseNode.
const ioInfo = asyncInfo.awaited;
const suspendedBySet = parentSuspenseNode.suspendedBy.get(ioInfo);
if (
suspendedBySet === undefined ||
!suspendedBySet.delete(instance)
) {
// A boundary can await the same IO multiple times.
// We still want to error if we're trying to remove IO that isn't present on
// this boundary so we need to check if we've already removed it.
// We're assuming previousSuspendedBy is a small array so this should be faster
// than allocating and maintaining a Set.
let alreadyRemovedIO = false;
for (let j = 0; j < i; j++) {
const removedIOInfo = previousSuspendedBy[j].awaited;
if (removedIOInfo === ioInfo) {
alreadyRemovedIO = true;
break;
}
}
if (!alreadyRemovedIO) {
throw new Error(
'We are cleaning up async info that was not on the parent Suspense boundary. ' +
'This is a bug in React.',
);
}
throw new Error(
'We are cleaning up async info that was not on the parent Suspense boundary. ' +
'This is a bug in React.',
);
}
if (suspendedBySet !== undefined && suspendedBySet.size === 0) {
if (suspendedBySet.size === 0) {
parentSuspenseNode.suspendedBy.delete(asyncInfo.awaited);
}
if (
@@ -3086,24 +3025,6 @@ export function attach(
}
}
function unmountSuspenseChildrenRecursively(
contentInstance: DevToolsInstance,
stashedSuspenseParent: null | SuspenseNode,
stashedSuspensePrevious: null | SuspenseNode,
stashedSuspenseRemaining: null | SuspenseNode,
): void {
// First unmount only the Offscreen boundary. I.e. the main content.
unmountInstanceRecursively(contentInstance);
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
// unmount the fallback, unmounting anything in the context of the parent SuspenseNode.
// Since the fallback conceptually blocks the parent.
reconcilingParentSuspenseNode = stashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
unmountRemainingChildren();
}
function isChildOf(
parentInstance: DevToolsInstance,
childInstance: DevToolsInstance,
@@ -3601,9 +3522,6 @@ export function attach(
// Unfortunately if we don't have any DEV time debug info or debug thenables then
// we have no meta data to show. However, we still mark this Suspense boundary as
// participating in the loading sequence since apparently it can suspend.
if (!suspenseNode.hasUniqueSuspenders) {
recordSuspenseSuspenders(suspenseNode);
}
suspenseNode.hasUniqueSuspenders = true;
// We have not seen any reason yet for why this suspense node might have been
// suspended but it clearly has been at some point. If we later discover a reason
@@ -4049,7 +3967,6 @@ export function attach(
debug('unmountInstanceRecursively()', instance, reconcilingParent);
}
let shouldPopSuspenseNode = false;
const stashedParent = reconcilingParent;
const stashedPrevious = previouslyReconciledSibling;
const stashedRemaining = remainingReconcilingChildren;
@@ -4070,46 +3987,11 @@ export function attach(
previouslyReconciledSiblingSuspenseNode = null;
remainingReconcilingChildrenSuspenseNodes =
instance.suspenseNode.firstChild;
shouldPopSuspenseNode = true;
}
try {
// Unmount the remaining set.
if (
(instance.kind === FIBER_INSTANCE ||
instance.kind === FILTERED_FIBER_INSTANCE) &&
instance.data.tag === SuspenseComponent &&
OffscreenComponent !== -1
) {
const fiber = instance.data;
const contentFiberInstance = remainingReconcilingChildren;
const hydrated = isFiberHydrated(fiber);
if (hydrated) {
if (contentFiberInstance === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
);
}
unmountSuspenseChildrenRecursively(
contentFiberInstance,
stashedSuspenseParent,
stashedSuspensePrevious,
stashedSuspenseRemaining,
);
// unmountSuspenseChildren already popped
shouldPopSuspenseNode = false;
} else {
if (contentFiberInstance !== null) {
throw new Error(
'A dehydrated Suspense node should not have a content Fiber.',
);
}
}
} else {
unmountRemainingChildren();
}
unmountRemainingChildren();
removePreviousSuspendedBy(
instance,
previousSuspendedBy,
@@ -4119,7 +4001,7 @@ export function attach(
reconcilingParent = stashedParent;
previouslyReconciledSibling = stashedPrevious;
remainingReconcilingChildren = stashedRemaining;
if (shouldPopSuspenseNode) {
if (instance.suspenseNode !== null) {
reconcilingParentSuspenseNode = stashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
@@ -4380,9 +4262,7 @@ export function attach(
virtualLevel + 1,
);
if ((updateFlags & ShouldResetChildren) !== NoUpdate) {
if (!isInDisconnectedSubtree) {
recordResetChildren(virtualInstance);
}
recordResetChildren(virtualInstance);
updateFlags &= ~ShouldResetChildren;
}
removePreviousSuspendedBy(
@@ -5169,9 +5049,7 @@ export function attach(
// We need to crawl the subtree for closest non-filtered Fibers
// so that we can display them in a flat children set.
if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) {
if (!nextIsHidden && !isInDisconnectedSubtree) {
recordResetChildren(fiberInstance);
}
recordResetChildren(fiberInstance);
// We've handled the child order change for this Fiber.
// Since it's included, there's no need to invalidate parent child order.
@@ -5352,7 +5230,7 @@ export function attach(
mountFiberRecursively(root.current, false);
flushPendingEvents();
flushPendingEvents(root);
needsToFlushComponentLogs = false;
currentRoot = (null: any);
@@ -5392,12 +5270,12 @@ export function attach(
root: FiberRoot,
priorityLevel: void | number,
) {
const nextFiber = root.current;
const current = root.current;
let prevFiber: null | Fiber = null;
let rootInstance = rootToFiberInstanceMap.get(root);
if (!rootInstance) {
rootInstance = createFiberInstance(nextFiber);
rootInstance = createFiberInstance(current);
rootToFiberInstanceMap.set(root, rootInstance);
idToDevToolsInstanceMap.set(rootInstance.id, rootInstance);
} else {
@@ -5436,28 +5314,30 @@ export function attach(
};
}
const nextIsMounted = nextFiber.child !== null;
const prevWasMounted = prevFiber !== null && prevFiber.child !== null;
if (!prevWasMounted && nextIsMounted) {
// Mount a new root.
setRootPseudoKey(currentRoot.id, nextFiber);
mountFiberRecursively(nextFiber, false);
} else if (prevWasMounted && nextIsMounted) {
if (prevFiber === null) {
throw new Error(
'Expected a previous Fiber when updating an existing root.',
);
if (prevFiber !== null) {
// TODO: relying on this seems a bit fishy.
const wasMounted =
prevFiber.memoizedState != null &&
prevFiber.memoizedState.element != null;
const isMounted =
current.memoizedState != null && current.memoizedState.element != null;
if (!wasMounted && isMounted) {
// Mount a new root.
setRootPseudoKey(currentRoot.id, current);
mountFiberRecursively(current, false);
} else if (wasMounted && isMounted) {
// Update an existing root.
updateFiberRecursively(rootInstance, current, prevFiber, false);
} else if (wasMounted && !isMounted) {
// Unmount an existing root.
unmountInstanceRecursively(rootInstance);
removeRootPseudoKey(currentRoot.id);
rootToFiberInstanceMap.delete(root);
}
// Update an existing root.
updateFiberRecursively(rootInstance, nextFiber, prevFiber, false);
} else if (prevWasMounted && !nextIsMounted) {
// Unmount an existing root.
unmountInstanceRecursively(rootInstance);
removeRootPseudoKey(currentRoot.id);
rootToFiberInstanceMap.delete(root);
} else if (!prevWasMounted && !nextIsMounted) {
// We don't need this root anymore.
rootToFiberInstanceMap.delete(root);
} else {
// Mount a new root.
setRootPseudoKey(currentRoot.id, current);
mountFiberRecursively(current, false);
}
if (isProfiling && isProfilingSupported) {
@@ -5481,7 +5361,7 @@ export function attach(
}
// We're done here.
flushPendingEvents();
flushPendingEvents(root);
needsToFlushComponentLogs = false;
@@ -5841,7 +5721,6 @@ export function attach(
function getSuspendedByOfSuspenseNode(
suspenseNode: SuspenseNode,
filterByChildInstance: null | DevToolsInstance, // only include suspended by instances in this subtree
): Array<SerializedAsyncInfo> {
// Collect all ReactAsyncInfo that was suspending this SuspenseNode but
// isn't also in any parent set.
@@ -5854,15 +5733,6 @@ export function attach(
// to a specific instance will have those appear in order of when that instance was discovered.
let hooksCacheKey: null | DevToolsInstance = null;
let hooksCache: null | HooksTree = null;
// Collect the stream entries with the highest byte offset and end time.
const streamEntries: Map<
Promise<mixed>,
{
asyncInfo: ReactAsyncInfo,
instance: DevToolsInstance,
hooks: null | HooksTree,
},
> = new Map();
suspenseNode.suspendedBy.forEach((set, ioInfo) => {
let parentNode = suspenseNode.parent;
while (parentNode !== null) {
@@ -5877,30 +5747,8 @@ export function attach(
if (set.size === 0) {
return;
}
let firstInstance: null | DevToolsInstance = null;
if (filterByChildInstance === null) {
firstInstance = (set.values().next().value: any);
} else {
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const childInstance of set.values()) {
if (firstInstance === null) {
firstInstance = childInstance;
}
if (
childInstance !== filterByChildInstance &&
!isChildOf(
filterByChildInstance,
childInstance,
suspenseNode.instance,
)
) {
// Something suspended on this outside the filtered instance. That means that
// it is not unique to just this filtered instance so we skip including it.
return;
}
}
}
if (firstInstance !== null && firstInstance.suspendedBy !== null) {
const firstInstance: DevToolsInstance = (set.values().next().value: any);
if (firstInstance.suspendedBy !== null) {
const asyncInfo = getAwaitInSuspendedByFromIO(
firstInstance.suspendedBy,
ioInfo,
@@ -5923,113 +5771,13 @@ export function attach(
}
}
}
const newIO = asyncInfo.awaited;
if (newIO.name === 'RSC stream' && newIO.value != null) {
const streamPromise = newIO.value;
// Special case RSC stream entries to pick the last entry keyed by the stream.
const existingEntry = streamEntries.get(streamPromise);
if (existingEntry === undefined) {
streamEntries.set(streamPromise, {
asyncInfo,
instance: firstInstance,
hooks,
});
} else {
const existingIO = existingEntry.asyncInfo.awaited;
if (
newIO !== existingIO &&
((newIO.byteSize !== undefined &&
existingIO.byteSize !== undefined &&
newIO.byteSize > existingIO.byteSize) ||
newIO.end > existingIO.end)
) {
// The new entry is later in the stream that the old entry. Replace it.
existingEntry.asyncInfo = asyncInfo;
existingEntry.instance = firstInstance;
existingEntry.hooks = hooks;
}
}
} else {
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
}
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
}
}
});
// Add any deduped stream entries.
streamEntries.forEach(({asyncInfo, instance, hooks}) => {
result.push(serializeAsyncInfo(asyncInfo, instance, hooks));
});
return result;
}
function getSuspendedByOfInstance(
devtoolsInstance: DevToolsInstance,
hooks: null | HooksTree,
): Array<SerializedAsyncInfo> {
const suspendedBy = devtoolsInstance.suspendedBy;
if (suspendedBy === null) {
return [];
}
const foundIOEntries: Set<ReactIOInfo> = new Set();
const streamEntries: Map<Promise<mixed>, ReactAsyncInfo> = new Map();
const result: Array<SerializedAsyncInfo> = [];
for (let i = 0; i < suspendedBy.length; i++) {
const asyncInfo = suspendedBy[i];
const ioInfo = asyncInfo.awaited;
if (foundIOEntries.has(ioInfo)) {
// We have already added this I/O entry to the result. We can dedupe it.
// This can happen when an instance depends on the same data in mutliple places.
continue;
}
foundIOEntries.add(ioInfo);
if (ioInfo.name === 'RSC stream' && ioInfo.value != null) {
const streamPromise = ioInfo.value;
// Special case RSC stream entries to pick the last entry keyed by the stream.
const existingEntry = streamEntries.get(streamPromise);
if (existingEntry === undefined) {
streamEntries.set(streamPromise, asyncInfo);
} else {
const existingIO = existingEntry.awaited;
if (
ioInfo !== existingIO &&
((ioInfo.byteSize !== undefined &&
existingIO.byteSize !== undefined &&
ioInfo.byteSize > existingIO.byteSize) ||
ioInfo.end > existingIO.end)
) {
// The new entry is later in the stream that the old entry. Replace it.
streamEntries.set(streamPromise, asyncInfo);
}
}
} else {
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
}
}
// Add any deduped stream entries.
streamEntries.forEach(asyncInfo => {
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
});
return result;
}
function getSuspendedByOfInstanceSubtree(
devtoolsInstance: DevToolsInstance,
): Array<SerializedAsyncInfo> {
// Get everything suspending below this instance down to the next Suspense node.
// First find the parent Suspense boundary which will have accumulated everything
let suspenseParentInstance = devtoolsInstance;
while (suspenseParentInstance.suspenseNode === null) {
if (suspenseParentInstance.parent === null) {
// We don't expect to hit this. We should always find the root.
return [];
}
suspenseParentInstance = suspenseParentInstance.parent;
}
const suspenseNode: SuspenseNode = suspenseParentInstance.suspenseNode;
return getSuspendedByOfSuspenseNode(suspenseNode, devtoolsInstance);
}
const FALLBACK_THROTTLE_MS: number = 300;
function getSuspendedByRange(
@@ -6543,17 +6291,17 @@ export function attach(
fiberInstance.suspenseNode !== null
? // If this is a Suspense boundary, then we include everything in the subtree that might suspend
// this boundary down to the next Suspense boundary.
getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode, null)
: tag === ActivityComponent
? // For Activity components we show everything that suspends the subtree down to the next boundary
// so that you can see what suspends a Transition at that level.
getSuspendedByOfInstanceSubtree(fiberInstance)
: // This set is an edge case where if you pass a promise to a Client Component into a children
// position without a Server Component as the direct parent. E.g. <div>{promise}</div>
// In this case, this becomes associated with the Client/Host Component where as normally
// you'd expect these to be associated with the Server Component that awaited the data.
// TODO: Prepend other suspense sources like css, images and use().
getSuspendedByOfInstance(fiberInstance, hooks);
getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode)
: // This set is an edge case where if you pass a promise to a Client Component into a children
// position without a Server Component as the direct parent. E.g. <div>{promise}</div>
// In this case, this becomes associated with the Client/Host Component where as normally
// you'd expect these to be associated with the Server Component that awaited the data.
// TODO: Prepend other suspense sources like css, images and use().
fiberInstance.suspendedBy === null
? []
: fiberInstance.suspendedBy.map(info =>
serializeAsyncInfo(info, fiberInstance, hooks),
);
const suspendedByRange = getSuspendedByRange(
getNearestSuspenseNode(fiberInstance),
);
@@ -6698,7 +6446,7 @@ export function attach(
const isSuspended = null;
// Things that Suspended this Server Component (use(), awaits and direct child promises)
const suspendedBy = getSuspendedByOfInstance(virtualInstance, null);
const suspendedBy = virtualInstance.suspendedBy;
const suspendedByRange = getSuspendedByRange(
getNearestSuspenseNode(virtualInstance),
);
@@ -6749,7 +6497,12 @@ export function attach(
? []
: Array.from(componentLogsEntry.warnings.entries()),
suspendedBy: suspendedBy,
suspendedBy:
suspendedBy === null
? []
: suspendedBy.map(info =>
serializeAsyncInfo(info, virtualInstance, null),
),
suspendedByRange: suspendedByRange,
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,

View File

@@ -28,7 +28,6 @@ export const SUSPENSE_TREE_OPERATION_ADD = 8;
export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
export const SUSPENSE_TREE_OPERATION_SUSPENDERS = 12;
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;

View File

@@ -24,7 +24,6 @@ import {
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
SUSPENSE_TREE_OPERATION_SUSPENDERS,
} from '../constants';
import {ElementTypeRoot} from '../frontend/types';
import {
@@ -112,7 +111,7 @@ export default class Store extends EventEmitter<{
roots: [],
rootSupportsBasicProfiling: [],
rootSupportsTimelineProfiling: [],
suspenseTreeMutated: [[Map<SuspenseNode['id'], SuspenseNode['id']>]],
suspenseTreeMutated: [],
supportsNativeStyleEditor: [],
supportsReloadAndProfile: [],
unsupportedBridgeProtocolDetected: [],
@@ -848,83 +847,6 @@ export default class Store extends EventEmitter<{
return list;
}
getSuspenseLineage(
suspenseID: SuspenseNode['id'],
): $ReadOnlyArray<SuspenseNode['id']> {
const lineage: Array<SuspenseNode['id']> = [];
let next: null | SuspenseNode = this.getSuspenseByID(suspenseID);
while (next !== null) {
if (next.parentID === 0) {
next = null;
} else {
lineage.unshift(next.id);
next = this.getSuspenseByID(next.parentID);
}
}
return lineage;
}
/**
* Like {@link getRootIDForElement} but should be used for traversing Suspense since it works with disconnected nodes.
*/
getSuspenseRootIDForSuspense(id: SuspenseNode['id']): number | null {
let current = this._idToSuspense.get(id);
while (current !== undefined) {
if (current.parentID === 0) {
return current.id;
} else {
current = this._idToSuspense.get(current.parentID);
}
}
return null;
}
/**
* @param rootID
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
*/
getSuspendableDocumentOrderSuspense(
rootID: Element['id'] | void,
uniqueSuspendersOnly: boolean,
): $ReadOnlyArray<SuspenseNode['id']> {
if (rootID === undefined) {
return [];
}
const root = this.getElementByID(rootID);
if (root === null) {
return [];
}
if (!this.supportsTogglingSuspense(root.id)) {
return [];
}
const list: SuspenseNode['id'][] = [];
const suspense = this.getSuspenseByID(root.id);
if (suspense !== null) {
const stack = [suspense];
while (stack.length > 0) {
const current = stack.pop();
if (current === undefined) {
continue;
}
// Include the root even if we won't show it suspended (because that's just blank).
// You should be able to see what suspended the shell.
if (!uniqueSuspendersOnly || current.hasUniqueSuspenders) {
list.push(current.id);
}
// Add children in reverse order to maintain document order
for (let j = current.children.length - 1; j >= 0; j--) {
const childSuspense = this.getSuspenseByID(current.children[j]);
if (childSuspense !== null) {
stack.push(childSuspense);
}
}
}
}
return list;
}
getRendererIDForElement(id: number): number | null {
let current = this._idToElement.get(id);
while (current !== undefined) {
@@ -1108,8 +1030,6 @@ export default class Store extends EventEmitter<{
const addedElementIDs: Array<number> = [];
// This is a mapping of removed ID -> parent ID:
const removedElementIDs: Map<number, number> = new Map();
const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =
new Map();
// We'll use the parent ID to adjust selection if it gets deleted.
let i = 2;
@@ -1588,7 +1508,6 @@ export default class Store extends EventEmitter<{
children: [],
name,
rects,
hasUniqueSuspenders: false,
});
hasSuspenseTreeChanged = true;
@@ -1622,7 +1541,6 @@ export default class Store extends EventEmitter<{
}
this._idToSuspense.delete(id);
removedSuspenseIDs.set(id, parentID);
let parentSuspense: ?SuspenseNode = null;
if (parentID === 0) {
@@ -1758,42 +1676,6 @@ export default class Store extends EventEmitter<{
break;
}
case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
const changeLength = operations[i + 1];
i += 2;
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
const id = operations[i];
const hasUniqueSuspenders = operations[i + 1] === 1;
const suspense = this._idToSuspense.get(id);
if (suspense === undefined) {
this._throwAndEmitError(
Error(
`Cannot update suspenders of suspense node "${id}" because no matching node was found in the Store.`,
),
);
break;
}
i += 2;
if (__DEBUG__) {
const previousHasUniqueSuspenders = suspense.hasUniqueSuspenders;
debug(
'Suspender changes',
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} (was ${String(previousHasUniqueSuspenders)})`,
);
}
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
}
hasSuspenseTreeChanged = true;
break;
}
default:
this._throwAndEmitError(
new UnsupportedBridgeOperationError(
@@ -1866,7 +1748,7 @@ export default class Store extends EventEmitter<{
}
if (hasSuspenseTreeChanged) {
this.emit('suspenseTreeMutated', [removedSuspenseIDs]);
this.emit('suspenseTreeMutated');
}
if (__DEBUG__) {

View File

@@ -52,7 +52,7 @@ type Props = {
type: IconType,
};
const panelIcons = '0 -960 960 820';
const materialIconsViewBox = '0 -960 960 960';
export default function ButtonIcon({className = '', type}: Props): React.Node {
let pathData = null;
let viewBox = '0 0 24 24';
@@ -131,27 +131,27 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
break;
case 'panel-left-close':
pathData = PATH_MATERIAL_PANEL_LEFT_CLOSE;
viewBox = panelIcons;
viewBox = materialIconsViewBox;
break;
case 'panel-left-open':
pathData = PATH_MATERIAL_PANEL_LEFT_OPEN;
viewBox = panelIcons;
viewBox = materialIconsViewBox;
break;
case 'panel-right-close':
pathData = PATH_MATERIAL_PANEL_RIGHT_CLOSE;
viewBox = panelIcons;
viewBox = materialIconsViewBox;
break;
case 'panel-right-open':
pathData = PATH_MATERIAL_PANEL_RIGHT_OPEN;
viewBox = panelIcons;
viewBox = materialIconsViewBox;
break;
case 'panel-bottom-open':
pathData = PATH_MATERIAL_PANEL_BOTTOM_OPEN;
viewBox = panelIcons;
viewBox = materialIconsViewBox;
break;
case 'panel-bottom-close':
pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE;
viewBox = panelIcons;
viewBox = materialIconsViewBox;
break;
case 'suspend':
pathData = PATH_SUSPEND;

View File

@@ -14,17 +14,8 @@ import styles from './Badge.css';
type Props = {
className?: string,
children: React$Node,
...
};
export default function Badge({
className = '',
children,
...props
}: Props): React.Node {
return (
<div {...props} className={`${styles.Badge} ${className}`}>
{children}
</div>
);
export default function Badge({className = '', children}: Props): React.Node {
return <div className={`${styles.Badge} ${className}`}>{children}</div>;
}

View File

@@ -8,34 +8,22 @@
*/
import * as React from 'react';
import {useState, useContext, useCallback} from 'react';
import {useContext} from 'react';
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import SearchInput from 'react-devtools-shared/src/devtools/views/SearchInput';
import {
TreeDispatcherContext,
TreeStateContext,
} from 'react-devtools-shared/src/devtools/views/Components/TreeContext';
import SearchInput from '../SearchInput';
export default function ComponentSearchInput(): React.Node {
const [localSearchQuery, setLocalSearchQuery] = useState('');
const {searchIndex, searchResults} = useContext(TreeStateContext);
const transitionDispatch = useContext(TreeDispatcherContext);
type Props = {};
const search = useCallback(
(text: string) => {
setLocalSearchQuery(text);
transitionDispatch({type: 'SET_SEARCH_TEXT', payload: text});
},
[setLocalSearchQuery, transitionDispatch],
);
const goToNextResult = useCallback(
() => transitionDispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}),
[transitionDispatch],
);
const goToPreviousResult = useCallback(
() => transitionDispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}),
[transitionDispatch],
);
export default function ComponentSearchInput(props: Props): React.Node {
const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const search = (text: string) =>
dispatch({type: 'SET_SEARCH_TEXT', payload: text});
const goToNextResult = () => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'});
const goToPreviousResult = () =>
dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'});
return (
<SearchInput
@@ -45,7 +33,7 @@ export default function ComponentSearchInput(): React.Node {
search={search}
searchIndex={searchIndex}
searchResultsCount={searchResults.length}
searchText={localSearchQuery}
searchText={searchText}
testName="ComponentSearchInput"
/>
);

View File

@@ -24,7 +24,6 @@ import type {Element as ElementType} from 'react-devtools-shared/src/frontend/ty
import styles from './Element.css';
import Icon from '../Icon';
import {useChangeOwnerAction} from './OwnersListContext';
import Tooltip from './reach-ui/tooltip';
type Props = {
data: ItemData,
@@ -232,16 +231,15 @@ export default function Element({data, index, style}: Props): React.Node {
/>
)}
{showStrictModeBadge && (
<Tooltip label="This component is not running in StrictMode.">
<Icon
className={
isSelected && treeFocused
? styles.StrictModeContrast
: styles.StrictMode
}
type="strict-mode-non-compliant"
/>
</Tooltip>
<Icon
className={
isSelected && treeFocused
? styles.StrictModeContrast
: styles.StrictMode
}
title="This component is not running in StrictMode."
type="strict-mode-non-compliant"
/>
)}
</div>
</div>

View File

@@ -11,3 +11,11 @@
position: absolute;
right: 0.25em;
}
.ForgetToggle {
display: flex;
}
.ForgetToggle > span { /* targets .ToggleContent */
padding: 0;
}

View File

@@ -11,7 +11,7 @@ import * as React from 'react';
import Badge from './Badge';
import IndexableDisplayName from './IndexableDisplayName';
import Tooltip from './reach-ui/tooltip';
import Toggle from '../Toggle';
import styles from './ForgetBadge.css';
@@ -40,11 +40,12 @@ export default function ForgetBadge(props: Props): React.Node {
'Memo'
);
const onChange = () => {};
const title =
'✨ This component has been auto-memoized by the React Compiler.';
return (
<Tooltip label={title}>
<Toggle onChange={onChange} className={styles.ForgetToggle} title={title}>
<Badge className={`${styles.Root} ${className}`}>{innerView}</Badge>
</Tooltip>
</Toggle>
);
}

View File

@@ -29,7 +29,6 @@ import InspectedElementViewSourceButton from './InspectedElementViewSourceButton
import useEditorURL from '../useEditorURL';
import styles from './InspectedElement.css';
import Tooltip from './reach-ui/tooltip';
export type Props = {};
@@ -193,15 +192,14 @@ export default function InspectedElementWrapper(_: Props): React.Node {
let strictModeBadge = null;
if (element.isStrictModeNonCompliant) {
strictModeBadge = (
<Tooltip label="This component is not running in StrictMode. Click to learn more.">
<a
className={styles.StrictModeNonCompliant}
href="https://react.dev/reference/react/StrictMode"
rel="noopener noreferrer"
target="_blank">
<Icon type="strict-mode-non-compliant" />
</a>
</Tooltip>
<a
className={styles.StrictModeNonCompliant}
href="https://react.dev/reference/react/StrictMode"
rel="noopener noreferrer"
target="_blank"
title="This component is not running in StrictMode. Click to learn more.">
<Icon type="strict-mode-non-compliant" />
</a>
);
}

View File

@@ -1,3 +1,11 @@
.Toggle {
display: flex;
}
.Toggle > span { /* targets .ToggleContent */
padding: 0;
}
.Badge {
cursor: help;
}

Some files were not shown because too many files have changed in this diff Show More