Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a59c9fc6bf | ||
|
|
ed1351c4fb | ||
|
|
93f8593289 | ||
|
|
dc1becd893 | ||
|
|
d8aa94b0f4 | ||
|
|
03ba0c76e1 | ||
|
|
4e00747378 | ||
|
|
7bd8716acd | ||
|
|
7385d1f61a | ||
|
|
85f415e33b | ||
|
|
903366b8b1 | ||
|
|
0fbb9b3683 | ||
|
|
e096403c59 | ||
|
|
1873ad7960 | ||
|
|
77b2f909f6 | ||
|
|
6773248311 | ||
|
|
5747cadf44 | ||
|
|
751edd6e2c | ||
|
|
6cfc9c1ff3 | ||
|
|
e7984651e4 | ||
|
|
5f2b571878 | ||
|
|
56e846921d | ||
|
|
19b71673b1 | ||
|
|
73507ec457 | ||
|
|
03a62b20fd | ||
|
|
b9ec735de2 | ||
|
|
47905a7950 | ||
|
|
7b971c0a55 | ||
|
|
83ea655a0b | ||
|
|
026abeaa5f | ||
|
|
d7215b4970 | ||
|
|
e2ce64acb9 | ||
|
|
34b1567427 | ||
|
|
b467c6e949 | ||
|
|
93d4458fdc | ||
|
|
1d68bce19c | ||
|
|
ead92181bd | ||
|
|
d44659744f | ||
|
|
8454a32f3c | ||
|
|
06fcc8f380 | ||
|
|
91e5c3daf1 | ||
|
|
4b3e662e4c | ||
|
|
3e1b34dc51 | ||
|
|
7568e71854 | ||
|
|
9724e3e66e | ||
|
|
848e0e3a4f | ||
|
|
5c15c1cd34 | ||
|
|
69b4cb8df4 | ||
|
|
a664f5f2ee | ||
|
|
1384ea8230 | ||
|
|
3025aa3964 | ||
|
|
a4eb2dfa6f | ||
|
|
6a8c7fb6f1 | ||
|
|
b65e6fc58b | ||
|
|
c786258422 | ||
|
|
1be3ce9996 | ||
|
|
3b2a398106 | ||
|
|
62ff1e61fc | ||
|
|
0e79784702 | ||
|
|
a2329c10ff | ||
|
|
d3f84a433a | ||
|
|
bc2356176b | ||
|
|
4fdf7cf249 | ||
|
|
614a945d9d | ||
|
|
d6eb735938 | ||
|
|
71753ac90a | ||
|
|
f24d3bbc70 | ||
|
|
85c427d822 | ||
|
|
02bd4458f7 |
11
.github/workflows/compiler_prereleases.yml
vendored
11
.github/workflows/compiler_prereleases.yml
vendored
@@ -19,6 +19,9 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
@@ -55,7 +58,13 @@ jobs:
|
||||
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Publish packages to npm
|
||||
- if: inputs.dry_run == true
|
||||
name: Publish packages to npm (dry run)
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
- if: inputs.dry_run != true
|
||||
name: Publish packages to npm
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
|
||||
@@ -17,6 +17,9 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -33,5 +36,6 @@ jobs:
|
||||
dist_tag: ${{ inputs.dist_tag }}
|
||||
version_name: ${{ inputs.version_name }}
|
||||
tag_version: ${{ inputs.tag_version }}
|
||||
dry_run: ${{ inputs.dry_run }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -19,5 +19,6 @@ jobs:
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
version_name: '0.0.0'
|
||||
dry_run: false
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
4
.github/workflows/runtime_prereleases.yml
vendored
4
.github/workflows/runtime_prereleases.yml
vendored
@@ -82,7 +82,6 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
@@ -91,11 +90,10 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
|
||||
@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '6.2.0',
|
||||
'eslint-plugin-react-hooks': '7.0.0',
|
||||
'jest-react': '0.18.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
|
||||
@@ -314,6 +314,36 @@ test('disableMemoizationForDebugging flag works as expected', async ({
|
||||
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
|
||||
});
|
||||
|
||||
test('error is displayed when source has syntax error', async ({page}) => {
|
||||
const syntaxErrorSource = `function TestComponent(props) {
|
||||
const oops = props.
|
||||
return (
|
||||
<>{oops}</>
|
||||
);
|
||||
}`;
|
||||
const store: Store = {
|
||||
source: syntaxErrorSource,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`);
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/08-source-syntax-error.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = text.join('');
|
||||
|
||||
expect(output.replace(/\s+/g, ' ')).toContain(
|
||||
'Expected identifier to be defined before being used',
|
||||
);
|
||||
});
|
||||
|
||||
TEST_CASE_INPUTS.forEach((t, idx) =>
|
||||
test(`playground compiles: ${t.name}`, async ({page}) => {
|
||||
const store: Store = {
|
||||
|
||||
@@ -22,7 +22,6 @@ export default function AccordionWindow(props: {
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
@@ -36,7 +35,6 @@ export default function AccordionWindow(props: {
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
isFailure={props.isFailure}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -51,7 +49,6 @@ function AccordionWindowItem({
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
isFailure,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
@@ -61,7 +58,7 @@ function AccordionWindowItem({
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
const id = useId();
|
||||
const isShow = isFailure ? name === 'Output' : tabsOpen.has(name);
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const transitionName = `accordion-window-item-${id}`;
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
useState,
|
||||
Suspense,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
@@ -35,6 +37,7 @@ import {BabelFileResult} from '@babel/core';
|
||||
import {
|
||||
CONFIG_PANEL_TRANSITION,
|
||||
TOGGLE_INTERNALS_TRANSITION,
|
||||
EXPAND_ACCORDION_TRANSITION,
|
||||
} from '../../lib/transitionTypes';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
@@ -265,8 +268,22 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
* changes between success/failure.
|
||||
*/
|
||||
|
||||
const [previousOutputKind, setPreviousOutputKind] = useState(
|
||||
compilerOutput.kind,
|
||||
);
|
||||
const isFailure = compilerOutput.kind !== 'ok';
|
||||
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
if (isFailure) {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
setTabsOpen(prev => new Set(prev).add('Output'));
|
||||
setActiveTab('Output');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
|
||||
let lastResult: string = '';
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
@@ -295,8 +312,6 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
// Display the Output tab on compilation failure
|
||||
activeTabOverride={isFailure ? 'Output' : undefined}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
@@ -315,7 +330,6 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
isFailure={isFailure}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
|
||||
@@ -17,15 +17,11 @@ export default function TabbedWindow({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
activeTabOverride,
|
||||
}: {
|
||||
tabs: Map<string, React.ReactNode>;
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
activeTabOverride?: string;
|
||||
}): React.ReactElement {
|
||||
const currentActiveTab = activeTabOverride ? activeTabOverride : activeTab;
|
||||
|
||||
const id = useId();
|
||||
const transitionName = `tab-highlight-${id}`;
|
||||
|
||||
@@ -41,7 +37,7 @@ export default function TabbedWindow({
|
||||
<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 = currentActiveTab === tab;
|
||||
const isActive = activeTab === tab;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
@@ -77,7 +73,7 @@ export default function TabbedWindow({
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(currentActiveTab)}
|
||||
{tabs.get(activeTab)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -297,7 +297,7 @@ export function compile(
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
if (error.hasErrors() || !transformOutput) {
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^2.1.0"
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"./**/@babel/parser": "7.7.4",
|
||||
|
||||
@@ -536,7 +536,8 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.StaticComponents:
|
||||
case ErrorCategory.Suppression:
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo: {
|
||||
case ErrorCategory.UseMemo:
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
@@ -582,6 +583,10 @@ export enum ErrorCategory {
|
||||
* Checking for valid usage of manual memoization
|
||||
*/
|
||||
UseMemo = 'UseMemo',
|
||||
/**
|
||||
* Checking that useMemos always return a value
|
||||
*/
|
||||
VoidUseMemo = 'VoidUseMemo',
|
||||
/**
|
||||
* Checking for higher order functions acting as factories for components/hooks
|
||||
*/
|
||||
@@ -669,6 +674,21 @@ export enum ErrorCategory {
|
||||
FBT = 'FBT',
|
||||
}
|
||||
|
||||
export enum LintRulePreset {
|
||||
/**
|
||||
* Rules that are stable and included in the `recommended` preset.
|
||||
*/
|
||||
Recommended = 'recommended',
|
||||
/**
|
||||
* Rules that are more experimental and only included in the `recommended-latest` preset.
|
||||
*/
|
||||
RecommendedLatest = 'recommended-latest',
|
||||
/**
|
||||
* Rules that are disabled.
|
||||
*/
|
||||
Off = 'off',
|
||||
}
|
||||
|
||||
export type LintRule = {
|
||||
// Stores the category the rule corresponds to, used to filter errors when reporting
|
||||
category: ErrorCategory;
|
||||
@@ -689,15 +709,14 @@ export type LintRule = {
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* If true, this rule will automatically appear in the default, "recommended" ESLint
|
||||
* rule set. Otherwise it will be part of an `allRules` export that developers can
|
||||
* use to opt-in to showing output of all possible rules.
|
||||
* Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
|
||||
* any preset.
|
||||
*
|
||||
* NOTE: not all validations are enabled by default! Setting this flag only affects
|
||||
* whether a given rule is part of the recommended set. The corresponding validation
|
||||
* also should be enabled by default if you want the error to actually show up!
|
||||
*/
|
||||
recommended: boolean;
|
||||
preset: LintRulePreset;
|
||||
};
|
||||
|
||||
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
|
||||
@@ -720,7 +739,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
@@ -730,7 +749,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Config: {
|
||||
@@ -739,7 +758,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
@@ -748,7 +767,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
@@ -758,7 +777,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectSetState: {
|
||||
@@ -768,7 +787,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'set-state-in-effect',
|
||||
description:
|
||||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.ErrorBoundaries: {
|
||||
@@ -778,7 +797,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'error-boundaries',
|
||||
description:
|
||||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Factories: {
|
||||
@@ -789,7 +808,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
description:
|
||||
'Validates against higher order functions defining nested components or hooks. ' +
|
||||
'Components and hooks should be defined at the module level',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
@@ -798,7 +817,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
@@ -807,7 +826,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
@@ -817,7 +836,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Globals: {
|
||||
@@ -828,7 +847,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
description:
|
||||
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
|
||||
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Hooks: {
|
||||
@@ -842,7 +861,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
|
||||
* this rule.
|
||||
*/
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Immutability: {
|
||||
@@ -852,7 +871,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'immutability',
|
||||
description:
|
||||
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Invariant: {
|
||||
@@ -861,7 +880,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.PreserveManualMemo: {
|
||||
@@ -873,7 +892,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
'Validates that existing manual memoized is preserved by the compiler. ' +
|
||||
'React Compiler will only compile components and hooks if its inference ' +
|
||||
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Purity: {
|
||||
@@ -883,7 +902,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'purity',
|
||||
description:
|
||||
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Refs: {
|
||||
@@ -893,7 +912,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'refs',
|
||||
description:
|
||||
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.RenderSetState: {
|
||||
@@ -903,7 +922,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'set-state-in-render',
|
||||
description:
|
||||
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.StaticComponents: {
|
||||
@@ -913,7 +932,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'static-components',
|
||||
description:
|
||||
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Suppression: {
|
||||
@@ -922,7 +941,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Syntax: {
|
||||
@@ -931,7 +950,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Todo: {
|
||||
@@ -940,7 +959,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Hint,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
recommended: false,
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
@@ -950,7 +969,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'unsupported-syntax',
|
||||
description:
|
||||
'Validates against syntax that we do not plan to support in React Compiler',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UseMemo: {
|
||||
@@ -960,7 +979,17 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'use-memo',
|
||||
description:
|
||||
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'void-use-memo',
|
||||
description:
|
||||
'Validates that useMemos always return a value. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
@@ -970,7 +999,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'incompatible-library',
|
||||
description:
|
||||
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
||||
recommended: true,
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
tryParseExternalFunction,
|
||||
} from '../HIR/Environment';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerPipelineValue} from './Pipeline';
|
||||
|
||||
const PanicThresholdOptionsSchema = z.enum([
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {ZodError, z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
@@ -159,7 +159,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* A function that, given the name of a module, can optionally return a description
|
||||
* of that module's type signature.
|
||||
*/
|
||||
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
moduleTypeProvider: z.nullable(z.any()).default(null),
|
||||
|
||||
/**
|
||||
* A list of functions which the application compiles as macros, where
|
||||
@@ -249,7 +249,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Allows specifying a function that can populate HIR with type information from
|
||||
* Flow
|
||||
*/
|
||||
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
flowTypeProvider: z.nullable(z.any()).default(null),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
@@ -906,6 +906,12 @@ export class Environment {
|
||||
if (moduleTypeProvider == null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof moduleTypeProvider !== 'function') {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Expected a function for \`moduleTypeProvider\``,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const unparsedModuleConfig = moduleTypeProvider(moduleName);
|
||||
if (unparsedModuleConfig != null) {
|
||||
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {isReservedWord} from '../Utils/Keyword';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {
|
||||
EffectSchema,
|
||||
|
||||
@@ -454,13 +454,13 @@ export function dropManualMemoization(
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This ${
|
||||
manualMemo.loadInstr.value.kind === 'PropertyLoad'
|
||||
? 'React.useMemo'
|
||||
: 'useMemo'
|
||||
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects`,
|
||||
? 'React.useMemo()'
|
||||
: 'useMemo()'
|
||||
} callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
|
||||
@@ -7,14 +7,17 @@
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
makeInstructionId,
|
||||
MutableRange,
|
||||
Place,
|
||||
ReactiveValue,
|
||||
ReactiveScope,
|
||||
} from '../HIR';
|
||||
import {Macro, MacroMethod} from '../HIR/Environment';
|
||||
import {eachReactiveValueOperand} from './visitors';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
|
||||
@@ -48,24 +51,49 @@ export function memoizeFbtAndMacroOperandsInSameScope(
|
||||
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
|
||||
...(fn.env.config.customMacros ?? []),
|
||||
]);
|
||||
const fbtValues: Set<IdentifierId> = new Set();
|
||||
/**
|
||||
* Set of all identifiers that load fbt or other macro functions or their nested
|
||||
* properties, as well as values known to be the results of invoking macros
|
||||
*/
|
||||
const macroTagsCalls: Set<IdentifierId> = new Set();
|
||||
/**
|
||||
* Mapping of lvalue => list of operands for all expressions where either
|
||||
* the lvalue is a known fbt/macro call and/or the operands transitively
|
||||
* contain fbt/macro calls.
|
||||
*
|
||||
* This is the key data structure that powers the scope merging: we start
|
||||
* at the lvalues and merge operands into the lvalue's scope.
|
||||
*/
|
||||
const macroValues: Map<Identifier, Array<Identifier>> = new Map();
|
||||
// Tracks methods loaded from macros, like fbt.param or idx.foo
|
||||
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
|
||||
while (true) {
|
||||
let vsize = fbtValues.size;
|
||||
let msize = macroMethods.size;
|
||||
visit(fn, fbtMacroTags, fbtValues, macroMethods);
|
||||
if (vsize === fbtValues.size && msize === macroMethods.size) {
|
||||
break;
|
||||
|
||||
visit(fn, fbtMacroTags, macroTagsCalls, macroMethods, macroValues);
|
||||
|
||||
for (const root of macroValues.keys()) {
|
||||
const scope = root.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
// Merge the operands into the same scope if this is a known macro invocation
|
||||
if (!macroTagsCalls.has(root.id)) {
|
||||
continue;
|
||||
}
|
||||
mergeScopes(root, scope, macroValues, macroTagsCalls);
|
||||
}
|
||||
return fbtValues;
|
||||
|
||||
return macroTagsCalls;
|
||||
}
|
||||
|
||||
export const FBT_TAGS: Set<string> = new Set([
|
||||
'fbt',
|
||||
'fbt:param',
|
||||
'fbt:enum',
|
||||
'fbt:plural',
|
||||
'fbs',
|
||||
'fbs:param',
|
||||
'fbs:enum',
|
||||
'fbs:plural',
|
||||
]);
|
||||
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||
'fbt:param',
|
||||
@@ -75,10 +103,22 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||
function visit(
|
||||
fn: HIRFunction,
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
|
||||
macroValues: Map<Identifier, Array<Identifier>>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const macroOperands: Array<Identifier> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
if (macroValues.has(operand.identifier)) {
|
||||
macroOperands.push(operand.identifier);
|
||||
}
|
||||
}
|
||||
if (macroOperands.length !== 0) {
|
||||
macroValues.set(phi.place.identifier, macroOperands);
|
||||
}
|
||||
}
|
||||
for (const instruction of block.instructions) {
|
||||
const {lvalue, value} = instruction;
|
||||
if (lvalue === null) {
|
||||
@@ -93,13 +133,13 @@ function visit(
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
* all `fbt` string literals in case they are used as a jsx tag.
|
||||
*/
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchesExactTag(value.binding.name, fbtMacroTags)
|
||||
) {
|
||||
// Record references to `fbt` as a global
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchTagRoot(value.binding.name, fbtMacroTags) !== null
|
||||
@@ -121,84 +161,66 @@ function visit(
|
||||
if (method.length > 1) {
|
||||
newMethods.push(method.slice(1));
|
||||
} else {
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newMethods.length > 0) {
|
||||
macroMethods.set(lvalue.identifier.id, newMethods);
|
||||
}
|
||||
} else if (isFbtCallExpression(fbtValues, value)) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* if the JSX element's tag was `fbt`, mark all its operands
|
||||
* to ensure that they end up in the same scope as the jsx element
|
||||
* itself.
|
||||
*/
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
fbtValues.add(operand.identifier.id);
|
||||
}
|
||||
} else if (
|
||||
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
|
||||
isFbtJsxChild(fbtValues, lvalue, value)
|
||||
value.kind === 'PropertyLoad' &&
|
||||
macroTagsCalls.has(value.object.identifier.id)
|
||||
) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* if the JSX element's tag was `fbt`, mark all its operands
|
||||
* to ensure that they end up in the same scope as the jsx element
|
||||
* itself.
|
||||
*/
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
|
||||
/*
|
||||
* NOTE: we add the operands as fbt values so that they are also
|
||||
* grouped with this expression
|
||||
*/
|
||||
fbtValues.add(operand.identifier.id);
|
||||
}
|
||||
} else if (fbtValues.has(lvalue.identifier.id)) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
if (
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
) {
|
||||
/*
|
||||
* named identifiers were already locals, we only have to force temporaries
|
||||
* into the same scope
|
||||
*/
|
||||
continue;
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
isFbtJsxExpression(fbtMacroTags, macroTagsCalls, value) ||
|
||||
isFbtJsxChild(macroTagsCalls, lvalue, value) ||
|
||||
isFbtCallExpression(macroTagsCalls, value)
|
||||
) {
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
macroValues.set(
|
||||
lvalue.identifier,
|
||||
Array.from(
|
||||
eachInstructionValueOperand(value),
|
||||
operand => operand.identifier,
|
||||
),
|
||||
);
|
||||
} else if (
|
||||
Iterable_some(eachInstructionValueOperand(value), operand =>
|
||||
macroValues.has(operand.identifier),
|
||||
)
|
||||
) {
|
||||
const macroOperands: Array<Identifier> = [];
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (macroValues.has(operand.identifier)) {
|
||||
macroOperands.push(operand.identifier);
|
||||
}
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
}
|
||||
macroValues.set(lvalue.identifier, macroOperands);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeScopes(
|
||||
root: Identifier,
|
||||
scope: ReactiveScope,
|
||||
macroValues: Map<Identifier, Array<Identifier>>,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
): void {
|
||||
const operands = macroValues.get(root);
|
||||
if (operands == null) {
|
||||
return;
|
||||
}
|
||||
for (const operand of operands) {
|
||||
operand.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.mutableRange);
|
||||
macroTagsCalls.add(operand.id);
|
||||
mergeScopes(operand, scope, macroValues, macroTagsCalls);
|
||||
}
|
||||
}
|
||||
|
||||
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
|
||||
return Array.from(tags).some(macro =>
|
||||
typeof macro === 'string'
|
||||
@@ -229,39 +251,40 @@ function matchTagRoot(
|
||||
}
|
||||
|
||||
function isFbtCallExpression(
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
value: InstructionValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'CallExpression' &&
|
||||
fbtValues.has(value.callee.identifier.id)) ||
|
||||
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
|
||||
macroTagsCalls.has(value.callee.identifier.id)) ||
|
||||
(value.kind === 'MethodCall' &&
|
||||
macroTagsCalls.has(value.property.identifier.id))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxExpression(
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
value: InstructionValue,
|
||||
): boolean {
|
||||
return (
|
||||
value.kind === 'JsxExpression' &&
|
||||
((value.tag.kind === 'Identifier' &&
|
||||
fbtValues.has(value.tag.identifier.id)) ||
|
||||
macroTagsCalls.has(value.tag.identifier.id)) ||
|
||||
(value.tag.kind === 'BuiltinTag' &&
|
||||
matchesExactTag(value.tag.name, fbtMacroTags)))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxChild(
|
||||
fbtValues: Set<IdentifierId>,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
lvalue: Place | null,
|
||||
value: ReactiveValue,
|
||||
value: InstructionValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
|
||||
lvalue !== null &&
|
||||
fbtValues.has(lvalue.identifier.id)
|
||||
macroTagsCalls.has(lvalue.identifier.id)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -393,7 +393,7 @@ function* generateInstructionTypes(
|
||||
shapeId: BuiltInArrayId,
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
|
||||
@@ -10,7 +10,16 @@ import {
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
@@ -18,8 +27,19 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
const useMemos = new Set<IdentifierId>();
|
||||
const react = new Set<IdentifierId>();
|
||||
const functions = new Map<IdentifierId, FunctionExpression>();
|
||||
const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const {lvalue, value} of block.instructions) {
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
/**
|
||||
* Most of the time useMemo results are referenced immediately. Don't bother
|
||||
* scanning instruction operands for useMemos unless there is an as-yet-unused
|
||||
* useMemo.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
unusedUseMemos.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
switch (value.kind) {
|
||||
case 'LoadGlobal': {
|
||||
if (value.binding.name === 'useMemo') {
|
||||
@@ -45,10 +65,8 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
case 'CallExpression': {
|
||||
// Is the function being called useMemo, with at least 1 argument?
|
||||
const callee =
|
||||
value.kind === 'CallExpression'
|
||||
? value.callee.identifier.id
|
||||
: value.property.identifier.id;
|
||||
const isUseMemo = useMemos.has(callee);
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const isUseMemo = useMemos.has(callee.identifier.id);
|
||||
if (!isUseMemo || value.args.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -104,10 +122,73 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
);
|
||||
}
|
||||
|
||||
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
|
||||
|
||||
if (fn.env.config.validateNoVoidUseMemo) {
|
||||
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
unusedUseMemos.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
/**
|
||||
* Basic check for unused memos, where the result of the call is never referenced. This runs
|
||||
* before DCE so it's more of an AST-level check that something, _anything_, cares about the value.
|
||||
*
|
||||
* This is easy to defeat with e.g. `const _ = useMemo(...)` but it at least gives us something to teach.
|
||||
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
|
||||
*/
|
||||
for (const loc of unusedUseMemos.values()) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'Unused useMemo()',
|
||||
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'useMemo() result is unused',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function validateNoContextVariableAssignment(
|
||||
fn: HIRFunction,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const value = instr.value;
|
||||
switch (value.kind) {
|
||||
case 'StoreContext': {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason:
|
||||
'useMemo() callbacks may not reassign variables declared outside of the callback',
|
||||
description:
|
||||
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: value.lvalue.place.loc,
|
||||
message: 'Cannot reassign variable',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
|
||||
validateHooksUsage: 1,
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage"."`,
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Invalid input: expected boolean, received number at "validateHooksUsage"."`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('parseConfigPragma()', () => {
|
||||
],
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: AutodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
let x;
|
||||
const y = useMemo(() => {
|
||||
let z;
|
||||
x = [];
|
||||
z = true;
|
||||
return z;
|
||||
}, []);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: useMemo() callbacks may not reassign variables declared outside of the callback
|
||||
|
||||
useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
|
||||
|
||||
error.invalid-reassign-variable-in-usememo.ts:5:4
|
||||
3 | const y = useMemo(() => {
|
||||
4 | let z;
|
||||
> 5 | x = [];
|
||||
| ^ Cannot reassign variable
|
||||
6 | z = true;
|
||||
7 | return z;
|
||||
8 | }, []);
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
function Component() {
|
||||
let x;
|
||||
const y = useMemo(() => {
|
||||
let z;
|
||||
x = [];
|
||||
z = true;
|
||||
return z;
|
||||
}, []);
|
||||
return [x, y];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
// Intentionally don't bind state, this repros a bug where we didn't
|
||||
// infer the type of destructured properties after a hole in the array
|
||||
let [, setState] = useState();
|
||||
setState(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
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-setState-in-render-unbound-state.ts:5:2
|
||||
3 | // infer the type of destructured properties after a hole in the array
|
||||
4 | let [, setState] = useState();
|
||||
> 5 | setState(1);
|
||||
| ^^^^^^^^ Found setState() in render
|
||||
6 | return props.foo;
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
function Component(props) {
|
||||
// Intentionally don't bind state, this repros a bug where we didn't
|
||||
// infer the type of destructured properties after a hole in the array
|
||||
let [, setState] = useState();
|
||||
setState(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Unused useMemo()
|
||||
|
||||
This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.invalid-unused-usememo.ts:3:2
|
||||
1 | // @validateNoVoidUseMemo
|
||||
2 | function Component() {
|
||||
> 3 | useMemo(() => {
|
||||
| ^^^^^^^ useMemo() result is unused
|
||||
4 | return [];
|
||||
5 | }, []);
|
||||
6 | return <div />;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// @validateNoVoidUseMemo
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ Found 2 errors:
|
||||
|
||||
Error: useMemo() callbacks must return a value
|
||||
|
||||
This useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
|
||||
This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.useMemo-no-return-value.ts:3:16
|
||||
1 | // @validateNoVoidUseMemo
|
||||
@@ -45,7 +45,7 @@ error.useMemo-no-return-value.ts:3:16
|
||||
|
||||
Error: useMemo() callbacks must return a value
|
||||
|
||||
This React.useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
|
||||
This React.useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.useMemo-no-return-value.ts:6:17
|
||||
4 | console.log('computing');
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<Stringify>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
---
|
||||
t3
|
||||
---
|
||||
```
|
||||
|
||||
|
||||
@@ -37,27 +37,31 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Foo(t0) {
|
||||
const $ = _c(3);
|
||||
const $ = _c(7);
|
||||
const { name1, name2 } = t0;
|
||||
let t1;
|
||||
if ($[0] !== name1 || $[1] !== name2) {
|
||||
let t2;
|
||||
if ($[3] !== name1) {
|
||||
t2 = <b>{name1}</b>;
|
||||
$[3] = name1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== name2) {
|
||||
t3 = <b>{name2}</b>;
|
||||
$[5] = name2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
t1 = fbt._(
|
||||
"{user1} and {user2} accepted your PR!",
|
||||
[
|
||||
fbt._param(
|
||||
"user1",
|
||||
|
||||
<span key={name1}>
|
||||
<b>{name1}</b>
|
||||
</span>,
|
||||
),
|
||||
fbt._param(
|
||||
"user2",
|
||||
|
||||
<span key={name2}>
|
||||
<b>{name2}</b>
|
||||
</span>,
|
||||
),
|
||||
fbt._param("user1", <span key={name1}>{t2}</span>),
|
||||
fbt._param("user2", <span key={name2}>{t3}</span>),
|
||||
],
|
||||
{ hk: "2PxMie" },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<Stringify>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(5);
|
||||
const { firstname, lastname } = t0;
|
||||
let t1;
|
||||
if ($[0] !== firstname || $[1] !== lastname) {
|
||||
t1 = fbt._(
|
||||
"Name: {firstname}, {lastname}",
|
||||
[
|
||||
fbt._param(
|
||||
"firstname",
|
||||
|
||||
<Stringify key={0} name={firstname} />,
|
||||
),
|
||||
fbt._param(
|
||||
"lastname",
|
||||
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt._("(inner fbt)", null, { hk: "36qNwF" })}
|
||||
</Stringify>,
|
||||
),
|
||||
],
|
||||
{ hk: "3AiIf8" },
|
||||
);
|
||||
$[0] = firstname;
|
||||
$[1] = lastname;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = <Stringify>{t1}</Stringify>;
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstname: "first", lastname: "last" }],
|
||||
sequentialRenders: [{ firstname: "first", lastname: "last" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"children":"Name: , "}</div>
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {fbt} from 'fbt';
|
||||
import {useState} from 'react';
|
||||
|
||||
const MIN = 10;
|
||||
|
||||
function Component() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return fbt(
|
||||
'Expected at least ' +
|
||||
fbt.param('min', MIN, {number: true}) +
|
||||
' items, but got ' +
|
||||
fbt.param('count', count, {number: true}) +
|
||||
' items.',
|
||||
'Error description'
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { fbt } from "fbt";
|
||||
import { useState } from "react";
|
||||
|
||||
const MIN = 10;
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const [count] = useState(0);
|
||||
let t0;
|
||||
if ($[0] !== count) {
|
||||
t0 = fbt._(
|
||||
{ "*": { "*": "Expected at least {min} items, but got {count} items." } },
|
||||
[
|
||||
fbt._param(
|
||||
"min",
|
||||
|
||||
MIN,
|
||||
[0],
|
||||
),
|
||||
fbt._param(
|
||||
"count",
|
||||
|
||||
count,
|
||||
[0],
|
||||
),
|
||||
],
|
||||
{ hk: "36gbz8" },
|
||||
);
|
||||
$[0] = count;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) Expected at least 10 items, but got 0 items.
|
||||
@@ -0,0 +1,22 @@
|
||||
import {fbt} from 'fbt';
|
||||
import {useState} from 'react';
|
||||
|
||||
const MIN = 10;
|
||||
|
||||
function Component() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return fbt(
|
||||
'Expected at least ' +
|
||||
fbt.param('min', MIN, {number: true}) +
|
||||
' items, but got ' +
|
||||
fbt.param('count', count, {number: true}) +
|
||||
' items.',
|
||||
'Error description'
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function t(props) {
|
||||
let [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function t(props) {
|
||||
const [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ["TodoAdd"],
|
||||
isComponent: "TodoAdd",
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
function t(props) {
|
||||
let [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
@@ -73,7 +73,7 @@ function Component(props) {
|
||||
const groupName4 = t3;
|
||||
let t4;
|
||||
if ($[8] !== props) {
|
||||
t4 = idx.hello_world.b.c(props, _temp3);
|
||||
t4 = idx.hello_world.b.c(props, (__3) => __3.group.label);
|
||||
$[8] = props;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
@@ -108,9 +108,6 @@ function Component(props) {
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
function _temp3(__3) {
|
||||
return __3.group.label;
|
||||
}
|
||||
function _temp2(__0) {
|
||||
return __0.group.label;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ function Component(props) {
|
||||
const groupName2 = t1;
|
||||
let t2;
|
||||
if ($[4] !== props) {
|
||||
t2 = idx.a.b(props, _temp2);
|
||||
t2 = idx.a.b(props, (__1) => __1.group.label);
|
||||
$[4] = props;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
@@ -74,9 +74,6 @@ function Component(props) {
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
function _temp2(__1) {
|
||||
return __1.group.label;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.group.label;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* isPending intentionally not captured */ start] = useTransition();
|
||||
|
||||
return useCallback(() => {
|
||||
start();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useCallback, useTransition } from "react";
|
||||
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
const [, start] = useTransition();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
start();
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) "[[ function params=0 ]]"
|
||||
@@ -0,0 +1,15 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* isPending intentionally not captured */ start] = useTransition();
|
||||
|
||||
return useCallback(() => {
|
||||
start();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* state value intentionally not captured */ setState] = useState();
|
||||
|
||||
return useCallback(() => {
|
||||
setState(x => x + 1);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useCallback, useTransition } from "react";
|
||||
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
const [, setState] = useState();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
setState(_temp);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp(x) {
|
||||
return x + 1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) useState is not defined
|
||||
@@ -0,0 +1,15 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* state value intentionally not captured */ setState] = useState();
|
||||
|
||||
return useCallback(() => {
|
||||
setState(x => x + 1);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
LintRules,
|
||||
LintRulePreset,
|
||||
type CompilerErrorDetailOptions,
|
||||
type CompilerDiagnosticOptions,
|
||||
type CompilerDiagnosticDetail,
|
||||
|
||||
@@ -120,7 +120,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
|
||||
|
||||
return <Child x={state} />;
|
||||
}`,
|
||||
errors: [makeTestCaseError('useMemo() callbacks must return a value')],
|
||||
errors: [makeTestCaseError('Unused useMemo()')],
|
||||
},
|
||||
{
|
||||
name: 'Pipeline errors are reported',
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"hermes-parser": "^0.25.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.0.3"
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.4",
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {Linter, Rule} from 'eslint';
|
||||
import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler';
|
||||
import {
|
||||
ErrorSeverity,
|
||||
LintRulePreset,
|
||||
LintRules,
|
||||
type LintRule,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
@@ -150,7 +151,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: rule.description,
|
||||
recommended: rule.recommended,
|
||||
recommended: rule.preset === LintRulePreset.Recommended,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
@@ -171,7 +172,16 @@ export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
|
||||
}, {} as RulesConfig);
|
||||
|
||||
export const recommendedRules: RulesConfig = LintRules.filter(
|
||||
rule => rule.recommended,
|
||||
rule => rule.preset === LintRulePreset.Recommended,
|
||||
).reduce((acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
}, {} as RulesConfig);
|
||||
|
||||
export const recommendedLatestRules: RulesConfig = LintRules.filter(
|
||||
rule =>
|
||||
rule.preset === LintRulePreset.Recommended ||
|
||||
rule.preset === LintRulePreset.RecommendedLatest,
|
||||
).reduce((acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
|
||||
@@ -10,7 +10,14 @@ import {defineConfig} from 'tsup';
|
||||
export default defineConfig({
|
||||
entry: ['./src/index.ts'],
|
||||
outDir: './dist',
|
||||
external: ['@babel/core', 'hermes-parser', 'zod', 'zod-validation-error'],
|
||||
external: [
|
||||
'@babel/core',
|
||||
'hermes-parser',
|
||||
'zod',
|
||||
'zod/v4',
|
||||
'zod-validation-error',
|
||||
'zod-validation-error/v4',
|
||||
],
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
dts: false,
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"fast-glob": "^3.3.2",
|
||||
"ora": "5.4.1",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.0.3"
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"engines": {
|
||||
|
||||
@@ -18,7 +18,9 @@ export default defineConfig({
|
||||
'ora',
|
||||
'yargs',
|
||||
'zod',
|
||||
'zod/v4',
|
||||
'zod-validation-error',
|
||||
'zod-validation-error/v4',
|
||||
],
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"html-to-text": "^9.0.5",
|
||||
"prettier": "^3.3.3",
|
||||
"puppeteer": "^24.7.2",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {z} from 'zod';
|
||||
import {z} from 'zod/v4';
|
||||
import {compile, type PrintedCompilerPipelineValue} from './compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
"react": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"readline": "^1.3.0",
|
||||
"yargs": "^17.7.1"
|
||||
"yargs": "^17.7.1",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.1",
|
||||
|
||||
@@ -9,8 +9,8 @@ import {render} from '@testing-library/react';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import React, {MutableRefObject} from 'react';
|
||||
import util from 'util';
|
||||
import {z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {initFbt, toJSON} from './shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,7 @@ async function main() {
|
||||
.option('tag', {
|
||||
description: 'Tag to publish to npm',
|
||||
type: 'choices',
|
||||
choices: ['experimental', 'beta', 'rc'],
|
||||
choices: ['experimental', 'beta', 'rc', 'latest'],
|
||||
default: 'experimental',
|
||||
})
|
||||
.option('tag-version', {
|
||||
@@ -145,10 +145,15 @@ async function main() {
|
||||
files: {exclude: ['.DS_Store']},
|
||||
});
|
||||
const truncatedHash = hash.slice(0, 7);
|
||||
let newVersion =
|
||||
argv.tagVersion == null || argv.tagVersion === ''
|
||||
? `${argv.versionName}-${argv.tag}`
|
||||
: `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
|
||||
let newVersion;
|
||||
if (argv.tag === 'latest') {
|
||||
newVersion = argv.versionName;
|
||||
} else {
|
||||
newVersion =
|
||||
argv.tagVersion == null || argv.tagVersion === ''
|
||||
? `${argv.versionName}-${argv.tag}`
|
||||
: `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
|
||||
}
|
||||
if (argv.tag === 'experimental' || argv.tag === 'beta') {
|
||||
newVersion = `${newVersion}-${truncatedHash}-${dateString}`;
|
||||
}
|
||||
@@ -181,21 +186,9 @@ async function main() {
|
||||
if (otp != null) {
|
||||
opts.push(`--otp=${otp}`);
|
||||
}
|
||||
/**
|
||||
* Typically, the `latest` tag is reserved for stable package versions. Since the the compiler
|
||||
* is still pre-release, until we have a stable release let's only add the
|
||||
* `latest` tag to non-experimental releases.
|
||||
*
|
||||
* `latest` is added by default, so we only override it for experimental releases so that
|
||||
* those don't get the `latest` tag.
|
||||
*
|
||||
* TODO: Update this when we have a stable release.
|
||||
*/
|
||||
if (argv.tag === 'experimental') {
|
||||
opts.push('--tag=experimental');
|
||||
} else {
|
||||
opts.push('--tag=latest');
|
||||
}
|
||||
|
||||
opts.push(`--tag=${argv.tag}`);
|
||||
|
||||
try {
|
||||
await spawnHelper(
|
||||
'npm',
|
||||
|
||||
@@ -10486,16 +10486,7 @@ string-length@^4.0.1:
|
||||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -10568,14 +10559,7 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -11352,7 +11336,7 @@ workerpool@^6.5.1:
|
||||
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
|
||||
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -11370,15 +11354,6 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||
@@ -11530,17 +11505,17 @@ zod-to-json-schema@^3.24.1:
|
||||
resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz"
|
||||
integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
|
||||
|
||||
zod-validation-error@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-2.1.0.tgz"
|
||||
integrity sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==
|
||||
"zod-validation-error@^3.5.0 || ^4.0.0":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
|
||||
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
|
||||
|
||||
zod-validation-error@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.0.3.tgz"
|
||||
integrity sha512-cETTrcMq3Ze58vhdR0zD37uJm/694I6mAxcf/ei5bl89cC++fBNxrC2z8lkFze/8hVMPwrbtrwXHR2LB50fpHw==
|
||||
|
||||
zod@^3.22.4, zod@^3.23.8, zod@^3.24.1:
|
||||
zod@^3.23.8, zod@^3.24.1:
|
||||
version "3.24.3"
|
||||
resolved "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz"
|
||||
integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==
|
||||
|
||||
"zod@^3.25.0 || ^4.0.0":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0"
|
||||
integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==
|
||||
|
||||
@@ -55,11 +55,11 @@ export default function ScrollIntoViewCase() {
|
||||
const scrollContainerRef = useRef(null);
|
||||
|
||||
const scrollVertical = () => {
|
||||
fragmentRef.current.experimental_scrollIntoView(alignToTop);
|
||||
fragmentRef.current.scrollIntoView(alignToTop);
|
||||
};
|
||||
|
||||
const scrollVerticalNoChildren = () => {
|
||||
noChildRef.current.experimental_scrollIntoView(alignToTop);
|
||||
noChildRef.current.scrollIntoView(alignToTop);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["plugin:react-hooks/recommended-latest-legacy"],
|
||||
"extends": ["plugin:react-hooks/recommended"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["plugin:react-hooks/recommended-latest-legacy"],
|
||||
"extends": ["plugin:react-hooks/recommended"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["plugin:react-hooks/recommended-latest-legacy"],
|
||||
"extends": ["plugin:react-hooks/recommended"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
|
||||
@@ -2,6 +2,7 @@ import {defineConfig} from 'eslint/config';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
||||
export default defineConfig([
|
||||
reactHooks.configs.flat['recommended-latest'],
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
@@ -12,10 +13,6 @@ export default defineConfig([
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
extends: ['react-hooks/recommended-latest'],
|
||||
rules: {
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
},
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.mjs && yarn",
|
||||
"lint": "eslint index.js --report-unused-disable-directives"
|
||||
"lint": "tsc --noEmit && eslint index.js --report-unused-disable-directives"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2022"
|
||||
],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"target": "es2022",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules",
|
||||
"../node_modules",
|
||||
"../../node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -48,13 +48,6 @@
|
||||
"@jridgewell/trace-mapping" "^0.3.28"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/helper-annotate-as-pure@^7.27.3":
|
||||
version "7.27.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5"
|
||||
integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==
|
||||
dependencies:
|
||||
"@babel/types" "^7.27.3"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.27.2":
|
||||
version "7.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d"
|
||||
@@ -66,32 +59,11 @@
|
||||
lru-cache "^5.1.1"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-create-class-features-plugin@^7.18.6":
|
||||
version "7.28.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz#3e747434ea007910c320c4d39a6b46f20f371d46"
|
||||
integrity sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure" "^7.27.3"
|
||||
"@babel/helper-member-expression-to-functions" "^7.27.1"
|
||||
"@babel/helper-optimise-call-expression" "^7.27.1"
|
||||
"@babel/helper-replace-supers" "^7.27.1"
|
||||
"@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
|
||||
"@babel/traverse" "^7.28.3"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-globals@^7.28.0":
|
||||
version "7.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674"
|
||||
integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
|
||||
|
||||
"@babel/helper-member-expression-to-functions@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44"
|
||||
integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==
|
||||
dependencies:
|
||||
"@babel/traverse" "^7.27.1"
|
||||
"@babel/types" "^7.27.1"
|
||||
|
||||
"@babel/helper-module-imports@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204"
|
||||
@@ -109,35 +81,6 @@
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
"@babel/traverse" "^7.28.3"
|
||||
|
||||
"@babel/helper-optimise-call-expression@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200"
|
||||
integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==
|
||||
dependencies:
|
||||
"@babel/types" "^7.27.1"
|
||||
|
||||
"@babel/helper-plugin-utils@^7.18.6":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c"
|
||||
integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==
|
||||
|
||||
"@babel/helper-replace-supers@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0"
|
||||
integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==
|
||||
dependencies:
|
||||
"@babel/helper-member-expression-to-functions" "^7.27.1"
|
||||
"@babel/helper-optimise-call-expression" "^7.27.1"
|
||||
"@babel/traverse" "^7.27.1"
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56"
|
||||
integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==
|
||||
dependencies:
|
||||
"@babel/traverse" "^7.27.1"
|
||||
"@babel/types" "^7.27.1"
|
||||
|
||||
"@babel/helper-string-parser@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
||||
@@ -168,14 +111,6 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.28.4"
|
||||
|
||||
"@babel/plugin-proposal-private-methods@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea"
|
||||
integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin" "^7.18.6"
|
||||
"@babel/helper-plugin-utils" "^7.18.6"
|
||||
|
||||
"@babel/template@^7.27.2":
|
||||
version "7.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
|
||||
@@ -198,7 +133,7 @@
|
||||
"@babel/types" "^7.28.4"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4":
|
||||
"@babel/types@^7.27.1", "@babel/types@^7.28.2", "@babel/types@^7.28.4":
|
||||
version "7.28.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a"
|
||||
integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==
|
||||
@@ -668,18 +603,6 @@ has-flag@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
hermes-estree@0.25.1:
|
||||
version "0.25.1"
|
||||
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
|
||||
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
|
||||
|
||||
hermes-parser@^0.25.1:
|
||||
version "0.25.1"
|
||||
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
|
||||
integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
|
||||
dependencies:
|
||||
hermes-estree "0.25.1"
|
||||
|
||||
ignore@^5.2.0:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
@@ -912,6 +835,11 @@ type-check@^0.4.0, type-check@~0.4.0:
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
|
||||
typescript@^5.4.3:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||
|
||||
update-browserslist-db@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
|
||||
@@ -949,12 +877,12 @@ yocto-queue@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zod-validation-error@^3.0.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.5.3.tgz#85ba33290200d8db9f043621e284f40dddefb7e5"
|
||||
integrity sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==
|
||||
"zod-validation-error@^3.0.3 || ^4.0.0":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
|
||||
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
|
||||
|
||||
zod@^3.22.4:
|
||||
version "3.25.76"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
|
||||
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||
"zod@^3.22.4 || ^4.0.0":
|
||||
version "4.1.11"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5"
|
||||
integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, {
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useState,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
addTransitionType,
|
||||
} from 'react';
|
||||
|
||||
import Chrome from './Chrome.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
addTransitionType,
|
||||
ViewTransition,
|
||||
Activity,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
## 7.0.0
|
||||
|
||||
This release slims down presets to just 2 configurations (`recommended` and `recommended-latest`), and all compiler rules are enabled by default.
|
||||
|
||||
- **Breaking:** Removed `recommended-latest-legacy` and `flat/recommended` configs. The plugin now provides `recommended` (legacy and flat configs with all recommended rules), and `recommended-latest` (legacy and flat configs with all recommended rules plus new bleeding edge experimental compiler rules). ([@poteto](https://github.com/poteto) in [#34757](https://github.com/facebook/react/pull/34757))
|
||||
|
||||
## 6.1.1
|
||||
|
||||
**Note:** 6.1.0 accidentally allowed use of `recommended` without flat config, causing errors when used with ESLint v9's `defineConfig()` helper. This has been fixed in 6.1.1.
|
||||
|
||||
- Fix `recommended` config for flat config compatibility. The `recommended` config has been converted to flat config format. Non-flat config users should use `recommended-legacy` instead. ([@poteto](https://github.com/poteto) in [#34700](https://github.com/facebook/react/pull/34700))
|
||||
- Add `recommended-latest` and `recommended-latest-legacy` configs that include React Compiler rules. ([@poteto](https://github.com/poteto) in [#34675](https://github.com/facebook/react/pull/34675))
|
||||
- Remove unused `NoUnusedOptOutDirectives` rule. ([@poteto](https://github.com/poteto) in [#34703](https://github.com/facebook/react/pull/34703))
|
||||
- Remove `hermes-parser` and dependency. ([@poteto](https://github.com/poteto) in [#34719](https://github.com/facebook/react/pull/34719))
|
||||
- Remove `@babel/plugin-proposal-private-methods` dependency. ([@ArnaudBarre](https://github.com/ArnaudBarre) and [@josephsavona](https://github.com/josephsavona) in [#34715](https://github.com/facebook/react/pull/34715))
|
||||
- Update for Zod v3/v4 compatibility. ([@kolian](https://github.com/kolvian) and [@josephsavona](https://github.com/josephsavona) in [#34717](https://github.com/facebook/react/pull/34717))
|
||||
|
||||
## 6.1.0
|
||||
|
||||
**Note:** Version 6.0.0 was mistakenly released and immediately deprecated and untagged on npm. This is the first official 6.x major release and includes breaking changes.
|
||||
**Note:** Version 6.0.0 was mistakenly released and immediately deprecated and untagged on npm. This is the first official 6.x major release and includes breaking changes.
|
||||
|
||||
- **Breaking:** Require Node.js 18 or newer. ([@michaelfaith](https://github.com/michaelfaith) in [#32458](https://github.com/facebook/react/pull/32458))
|
||||
- **Breaking:** Flat config is now the default `recommended` preset. Legacy config moved to `recommended-legacy`. ([@michaelfaith](https://github.com/michaelfaith) in [#32457](https://github.com/facebook/react/pull/32457))
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
# `eslint-plugin-react-hooks`
|
||||
|
||||
This ESLint plugin enforces the [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks).
|
||||
|
||||
It is a part of the [Hooks API](https://react.dev/reference/react/hooks) for React.
|
||||
The official ESLint plugin for [React](https://react.dev) which enforces the [Rules of React](https://react.dev/reference/eslint-plugin-react-hooks) and other best practices.
|
||||
|
||||
## Installation
|
||||
|
||||
**Note: If you're using Create React App, please use `react-scripts` >= 3 instead of adding it directly.**
|
||||
|
||||
Assuming you already have ESLint installed, run:
|
||||
|
||||
```sh
|
||||
@@ -20,9 +16,7 @@ yarn add eslint-plugin-react-hooks --dev
|
||||
|
||||
### Flat Config (eslint.config.js|ts)
|
||||
|
||||
#### >= 6.0.0
|
||||
|
||||
For users of 6.0 and beyond, add the `recommended` config.
|
||||
Add the `recommended` config for all recommended rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
@@ -30,71 +24,42 @@ import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
extends: ['react-hooks/recommended'],
|
||||
},
|
||||
reactHooks.configs.flat.recommended,
|
||||
]);
|
||||
```
|
||||
|
||||
#### 5.2.0
|
||||
|
||||
For users of 5.2.0 (the first version with flat config support), add the `recommended-latest` config.
|
||||
If you want to try bleeding edge experimental compiler rules, use `recommended-latest`.
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
extends: ['react-hooks/recommended-latest'],
|
||||
},
|
||||
reactHooks.configs.flat['recommended-latest'],
|
||||
]);
|
||||
```
|
||||
|
||||
### Legacy Config (.eslintrc)
|
||||
|
||||
#### >= 5.2.0
|
||||
|
||||
If you are still using ESLint below 9.0.0, you can use `recommended-legacy` for accessing a legacy version of the recommended config.
|
||||
If you are still using ESLint below 9.0.0, the `recommended` preset can also be used to enable all recommended rules.
|
||||
|
||||
```js
|
||||
{
|
||||
"extends": [
|
||||
// ...
|
||||
"plugin:react-hooks/recommended-legacy"
|
||||
]
|
||||
"extends": ["plugin:react-hooks/recommended"],
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### < 5.2.0
|
||||
|
||||
If you're using a version earlier than 5.2.0, the legacy config was simply `recommended`.
|
||||
|
||||
```js
|
||||
{
|
||||
"extends": [
|
||||
// ...
|
||||
"plugin:react-hooks/recommended"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
If you want more fine-grained configuration, you can instead add a snippet like this to your ESLint configuration file:
|
||||
If you want more fine-grained configuration, you can instead choose to enable specific rules. However, we strongly encourage using the recommended presets — see above — so that you will automatically receive new recommended rules as we add them in future versions of the plugin.
|
||||
|
||||
#### Flat Config (eslint.config.js|ts)
|
||||
|
||||
```js
|
||||
import * as reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -102,8 +67,26 @@ export default [
|
||||
plugins: { 'react-hooks': reactHooks },
|
||||
// ...
|
||||
rules: {
|
||||
// Core hooks rules
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
|
||||
// React Compiler rules
|
||||
'react-hooks/config': 'error',
|
||||
'react-hooks/error-boundaries': 'error',
|
||||
'react-hooks/component-hook-factories': 'error',
|
||||
'react-hooks/gating': 'error',
|
||||
'react-hooks/globals': 'error',
|
||||
'react-hooks/immutability': 'error',
|
||||
'react-hooks/preserve-manual-memoization': 'error',
|
||||
'react-hooks/purity': 'error',
|
||||
'react-hooks/refs': 'error',
|
||||
'react-hooks/set-state-in-effect': 'error',
|
||||
'react-hooks/set-state-in-render': 'error',
|
||||
'react-hooks/static-components': 'error',
|
||||
'react-hooks/unsupported-syntax': 'warn',
|
||||
'react-hooks/use-memo': 'error',
|
||||
'react-hooks/incompatible-library': 'warn',
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -118,8 +101,26 @@ export default [
|
||||
],
|
||||
"rules": {
|
||||
// ...
|
||||
// Core hooks rules
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
|
||||
// React Compiler rules
|
||||
"react-hooks/config": "error",
|
||||
"react-hooks/error-boundaries": "error",
|
||||
"react-hooks/component-hook-factories": "error",
|
||||
"react-hooks/gating": "error",
|
||||
"react-hooks/globals": "error",
|
||||
"react-hooks/immutability": "error",
|
||||
"react-hooks/preserve-manual-memoization": "error",
|
||||
"react-hooks/purity": "error",
|
||||
"react-hooks/refs": "error",
|
||||
"react-hooks/set-state-in-effect": "error",
|
||||
"react-hooks/set-state-in-render": "error",
|
||||
"react-hooks/static-components": "error",
|
||||
"react-hooks/unsupported-syntax": "warn",
|
||||
"react-hooks/use-memo": "error",
|
||||
"react-hooks/incompatible-library": "warn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export * from './cjs/eslint-plugin-react-hooks';
|
||||
export {default} from './cjs/eslint-plugin-react-hooks';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-plugin-react-hooks",
|
||||
"description": "ESLint rules for React Hooks",
|
||||
"version": "5.2.0",
|
||||
"version": "7.0.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
@@ -42,22 +42,22 @@
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"hermes-parser": "^0.25.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.0.3"
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.11.4",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@babel/types": "^7.19.0",
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/estree-jsx": "^1.0.5",
|
||||
"@types/node": "^20.2.5",
|
||||
"@typescript-eslint/parser-v2": "npm:@typescript-eslint/parser@^2.26.0",
|
||||
"@typescript-eslint/parser-v3": "npm:@typescript-eslint/parser@^3.10.0",
|
||||
"@typescript-eslint/parser-v4": "npm:@typescript-eslint/parser@^4.1.0",
|
||||
"@typescript-eslint/parser-v5": "npm:@typescript-eslint/parser@^5.62.0",
|
||||
"@types/eslint": "^8.56.12",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/estree-jsx": "^1.0.5",
|
||||
"@types/node": "^20.2.5",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"eslint-v7": "npm:eslint@^7.7.0",
|
||||
"eslint-v8": "npm:eslint@^8.57.1",
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
allRules,
|
||||
mapErrorSeverityToESlint,
|
||||
recommendedRules,
|
||||
recommendedLatestRules,
|
||||
} from './shared/ReactCompiler';
|
||||
import RulesOfHooks from './rules/RulesOfHooks';
|
||||
|
||||
// All rules
|
||||
const rules = {
|
||||
'exhaustive-deps': ExhaustiveDeps,
|
||||
'rules-of-hooks': RulesOfHooks,
|
||||
@@ -23,13 +23,12 @@ const rules = {
|
||||
),
|
||||
} satisfies Record<string, Rule.RuleModule>;
|
||||
|
||||
// Basic hooks rules (for recommended config)
|
||||
const basicRuleConfigs = {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
} as const satisfies Linter.RulesRecord;
|
||||
|
||||
const compilerRuleConfigs = Object.fromEntries(
|
||||
const recommendedCompilerRuleConfigs = Object.fromEntries(
|
||||
Object.entries(recommendedRules).map(([name, ruleConfig]) => {
|
||||
return [
|
||||
`react-hooks/${name}` as const,
|
||||
@@ -38,69 +37,61 @@ const compilerRuleConfigs = Object.fromEntries(
|
||||
}),
|
||||
) as Record<`react-hooks/${string}`, Linter.RuleEntry>;
|
||||
|
||||
// All rules including compiler rules (for recommended-latest config)
|
||||
const allRuleConfigs: Linter.RulesRecord = {
|
||||
const recommendedLatestCompilerRuleConfigs = Object.fromEntries(
|
||||
Object.entries(recommendedLatestRules).map(([name, ruleConfig]) => {
|
||||
return [
|
||||
`react-hooks/${name}` as const,
|
||||
mapErrorSeverityToESlint(ruleConfig.severity),
|
||||
] as const;
|
||||
}),
|
||||
) as Record<`react-hooks/${string}`, Linter.RuleEntry>;
|
||||
|
||||
const recommendedRuleConfigs: Linter.RulesRecord = {
|
||||
...basicRuleConfigs,
|
||||
...compilerRuleConfigs,
|
||||
...recommendedCompilerRuleConfigs,
|
||||
};
|
||||
const recommendedLatestRuleConfigs: Linter.RulesRecord = {
|
||||
...basicRuleConfigs,
|
||||
...recommendedLatestCompilerRuleConfigs,
|
||||
};
|
||||
|
||||
const plugins = ['react-hooks'];
|
||||
|
||||
type ReactHooksFlatConfig = {
|
||||
plugins: {react: any};
|
||||
rules: Linter.RulesRecord;
|
||||
};
|
||||
|
||||
const configs = {
|
||||
recommended: {
|
||||
plugins,
|
||||
rules: recommendedRuleConfigs,
|
||||
},
|
||||
'recommended-latest': {
|
||||
plugins,
|
||||
rules: recommendedLatestRuleConfigs,
|
||||
},
|
||||
flat: {} as Record<string, ReactHooksFlatConfig>,
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
meta: {
|
||||
name: 'eslint-plugin-react-hooks',
|
||||
version: '7.0.0',
|
||||
},
|
||||
rules,
|
||||
configs: {} as {
|
||||
'recommended-legacy': {
|
||||
plugins: Array<string>;
|
||||
rules: Linter.RulesRecord;
|
||||
};
|
||||
'recommended-latest-legacy': {
|
||||
plugins: Array<string>;
|
||||
rules: Linter.RulesRecord;
|
||||
};
|
||||
'flat/recommended': Array<Linter.Config>;
|
||||
'recommended-latest': Array<Linter.Config>;
|
||||
recommended: Array<Linter.Config>;
|
||||
},
|
||||
configs,
|
||||
};
|
||||
|
||||
Object.assign(plugin.configs, {
|
||||
'recommended-legacy': {
|
||||
plugins: ['react-hooks'],
|
||||
rules: basicRuleConfigs,
|
||||
Object.assign(configs.flat, {
|
||||
'recommended-latest': {
|
||||
plugins: {'react-hooks': plugin},
|
||||
rules: configs['recommended-latest'].rules,
|
||||
},
|
||||
|
||||
'recommended-latest-legacy': {
|
||||
plugins: ['react-hooks'],
|
||||
rules: allRuleConfigs,
|
||||
recommended: {
|
||||
plugins: {'react-hooks': plugin},
|
||||
rules: configs.recommended.rules,
|
||||
},
|
||||
|
||||
'flat/recommended': [
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': plugin,
|
||||
},
|
||||
rules: basicRuleConfigs,
|
||||
},
|
||||
],
|
||||
|
||||
'recommended-latest': [
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': plugin,
|
||||
},
|
||||
rules: allRuleConfigs,
|
||||
},
|
||||
],
|
||||
|
||||
recommended: [
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': plugin,
|
||||
},
|
||||
rules: basicRuleConfigs,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default plugin;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
LintRules,
|
||||
type LintRule,
|
||||
ErrorSeverity,
|
||||
LintRulePreset,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {type Linter, type Rule} from 'eslint';
|
||||
import runReactCompiler, {RunCacheEntry} from './RunReactCompiler';
|
||||
@@ -149,7 +150,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: rule.description,
|
||||
recommended: rule.recommended,
|
||||
recommended: rule.preset === LintRulePreset.Recommended,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
@@ -164,23 +165,26 @@ type RulesConfig = {
|
||||
[name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
|
||||
};
|
||||
|
||||
export const allRules: RulesConfig = LintRules.reduce(
|
||||
(acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
},
|
||||
{} as RulesConfig,
|
||||
);
|
||||
export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
}, {} as RulesConfig);
|
||||
|
||||
export const recommendedRules: RulesConfig = LintRules.filter(
|
||||
rule => rule.recommended,
|
||||
).reduce(
|
||||
(acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
},
|
||||
{} as RulesConfig,
|
||||
);
|
||||
rule => rule.preset === LintRulePreset.Recommended,
|
||||
).reduce((acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
}, {} as RulesConfig);
|
||||
|
||||
export const recommendedLatestRules: RulesConfig = LintRules.filter(
|
||||
rule =>
|
||||
rule.preset === LintRulePreset.Recommended ||
|
||||
rule.preset === LintRulePreset.RecommendedLatest,
|
||||
).reduce((acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
}, {} as RulesConfig);
|
||||
|
||||
export function mapErrorSeverityToESlint(
|
||||
severity: ErrorSeverity,
|
||||
|
||||
@@ -78,7 +78,6 @@ function getFlowSuppressions(
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
function runReactCompilerImpl({
|
||||
sourceCode,
|
||||
filename,
|
||||
@@ -115,6 +114,7 @@ function runReactCompilerImpl({
|
||||
}
|
||||
|
||||
let babelAST: ParseResult<File> | null = null;
|
||||
|
||||
if (filename.endsWith('.tsx') || filename.endsWith('.ts')) {
|
||||
try {
|
||||
babelAST = babelParse(sourceCode.text, {
|
||||
|
||||
99
packages/react-client/src/ReactFlightClient.js
vendored
99
packages/react-client/src/ReactFlightClient.js
vendored
@@ -367,6 +367,7 @@ type Response = {
|
||||
_debugRootStack?: null | Error, // DEV-only
|
||||
_debugRootTask?: null | ConsoleTask, // DEV-only
|
||||
_debugStartTime: number, // DEV-only
|
||||
_debugIOStarted: boolean, // DEV-only
|
||||
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
|
||||
_debugChannel?: void | DebugChannel, // DEV-only
|
||||
_blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
|
||||
@@ -500,7 +501,7 @@ function createErrorChunk<T>(
|
||||
}
|
||||
|
||||
function moveDebugInfoFromChunkToInnerValue<T>(
|
||||
chunk: InitializedChunk<T>,
|
||||
chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
|
||||
value: T,
|
||||
): void {
|
||||
// Remove the debug info from the initialized chunk, and add it to the inner
|
||||
@@ -1569,6 +1570,10 @@ function fulfillReference(
|
||||
initializedChunk.reason = handler.reason; // Used by streaming chunks
|
||||
if (resolveListeners !== null) {
|
||||
wakeChunk(resolveListeners, handler.value, initializedChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1818,6 +1823,10 @@ function loadServerReference<A: Iterable<any>, T>(
|
||||
initializedChunk.value = handler.value;
|
||||
if (resolveListeners !== null) {
|
||||
wakeChunk(resolveListeners, handler.value, initializedChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2536,6 +2545,10 @@ function missingCall() {
|
||||
);
|
||||
}
|
||||
|
||||
function markIOStarted(this: Response) {
|
||||
this._debugIOStarted = true;
|
||||
}
|
||||
|
||||
function ResponseInstance(
|
||||
this: $FlowFixMe,
|
||||
bundlerConfig: ServerConsumerModuleMap,
|
||||
@@ -2609,6 +2622,10 @@ function ResponseInstance(
|
||||
// where as if you use createFromReadableStream from the body of the fetch
|
||||
// then the start time is when the headers resolved.
|
||||
this._debugStartTime = performance.now();
|
||||
this._debugIOStarted = false;
|
||||
// We consider everything before the first setTimeout task to be cached data
|
||||
// and is not considered I/O required to load the stream.
|
||||
setTimeout(markIOStarted.bind(this), 0);
|
||||
}
|
||||
this._debugFindSourceMapURL = findSourceMapURL;
|
||||
this._debugChannel = debugChannel;
|
||||
@@ -2762,7 +2779,7 @@ function incrementChunkDebugInfo(
|
||||
}
|
||||
}
|
||||
|
||||
function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
|
||||
function addAsyncInfo(chunk: SomeChunk<any>, asyncInfo: ReactAsyncInfo): void {
|
||||
const value = resolveLazy(chunk.value);
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
@@ -2774,34 +2791,39 @@ function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
|
||||
) {
|
||||
if (isArray(value._debugInfo)) {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
value._debugInfo.push.apply(value._debugInfo, debugInfo);
|
||||
value._debugInfo.push(asyncInfo);
|
||||
} else {
|
||||
Object.defineProperty((value: any), '_debugInfo', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: debugInfo,
|
||||
value: [asyncInfo],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
chunk._debugInfo.push.apply(chunk._debugInfo, debugInfo);
|
||||
chunk._debugInfo.push(asyncInfo);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChunkDebugInfo(
|
||||
response: Response,
|
||||
streamState: StreamState,
|
||||
chunk: SomeChunk<any>,
|
||||
): void {
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
// Add the currently resolving chunk's debug info representing the stream
|
||||
// to the Promise that was waiting on the stream, or its underlying value.
|
||||
const debugInfo: ReactDebugInfo = [{awaited: streamState._debugInfo}];
|
||||
if (chunk.status === PENDING || chunk.status === BLOCKED) {
|
||||
const boundAddDebugInfo = addDebugInfo.bind(null, chunk, debugInfo);
|
||||
chunk.then(boundAddDebugInfo, boundAddDebugInfo);
|
||||
} else {
|
||||
addDebugInfo(chunk, debugInfo);
|
||||
// Only include stream information after a macrotask. Any chunk processed
|
||||
// before that is considered cached data.
|
||||
if (response._debugIOStarted) {
|
||||
// Add the currently resolving chunk's debug info representing the stream
|
||||
// to the Promise that was waiting on the stream, or its underlying value.
|
||||
const asyncInfo: ReactAsyncInfo = {awaited: streamState._debugInfo};
|
||||
if (chunk.status === PENDING || chunk.status === BLOCKED) {
|
||||
const boundAddAsyncInfo = addAsyncInfo.bind(null, chunk, asyncInfo);
|
||||
chunk.then(boundAddAsyncInfo, boundAddAsyncInfo);
|
||||
} else {
|
||||
addAsyncInfo(chunk, asyncInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2837,12 +2859,12 @@ function resolveModel(
|
||||
model,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
}
|
||||
resolveModelChunk(response, chunk, model);
|
||||
}
|
||||
@@ -2869,7 +2891,7 @@ function resolveText(
|
||||
}
|
||||
const newChunk = createInitializedTextChunk(response, text);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
}
|
||||
@@ -2895,7 +2917,7 @@ function resolveBuffer(
|
||||
}
|
||||
const newChunk = createInitializedBufferChunk(response, buffer);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
}
|
||||
@@ -2942,7 +2964,7 @@ function resolveModule(
|
||||
blockedChunk.status = BLOCKED;
|
||||
}
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, blockedChunk);
|
||||
resolveChunkDebugInfo(response, streamState, blockedChunk);
|
||||
}
|
||||
promise.then(
|
||||
() => resolveModuleChunk(response, blockedChunk, clientReference),
|
||||
@@ -2952,12 +2974,12 @@ function resolveModule(
|
||||
if (!chunk) {
|
||||
const newChunk = createResolvedModuleChunk(response, clientReference);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
}
|
||||
// This can't actually happen because we don't have any forward
|
||||
// references to modules.
|
||||
@@ -2978,13 +3000,13 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
|
||||
if (!chunk) {
|
||||
const newChunk = createInitializedStreamChunk(response, stream, controller);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
return;
|
||||
}
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
}
|
||||
if (chunk.status !== PENDING) {
|
||||
// We already resolved. We didn't expect to see this.
|
||||
@@ -3034,6 +3056,10 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
|
||||
resolvedChunk.reason = controller;
|
||||
if (resolveListeners !== null) {
|
||||
wakeChunk(resolveListeners, chunk.value, (chunk: any));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(resolvedChunk, stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3433,12 +3459,12 @@ function resolvePostponeDev(
|
||||
postponeInstance,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
@@ -3467,12 +3493,12 @@ function resolveErrorModel(
|
||||
errorWithDigest,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, errorWithDigest);
|
||||
}
|
||||
@@ -4318,7 +4344,26 @@ function flushComponentPerformance(
|
||||
|
||||
// First find the start time of the first component to know if it was running
|
||||
// in parallel with the previous.
|
||||
const debugInfo = __DEV__ && root._debugInfo;
|
||||
let debugInfo = null;
|
||||
if (__DEV__) {
|
||||
debugInfo = root._debugInfo;
|
||||
if (debugInfo.length === 0 && root.status === 'fulfilled') {
|
||||
const resolvedValue = resolveLazy(root.value);
|
||||
if (
|
||||
typeof resolvedValue === 'object' &&
|
||||
resolvedValue !== null &&
|
||||
(isArray(resolvedValue) ||
|
||||
typeof resolvedValue[ASYNC_ITERATOR] === 'function' ||
|
||||
resolvedValue.$$typeof === REACT_ELEMENT_TYPE ||
|
||||
resolvedValue.$$typeof === REACT_LAZY_TYPE) &&
|
||||
isArray(resolvedValue._debugInfo)
|
||||
) {
|
||||
// It's possible that the value has been given the debug info.
|
||||
// In that case we need to look for it on the resolved value.
|
||||
debugInfo = resolvedValue._debugInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (debugInfo) {
|
||||
let startTime = 0;
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
|
||||
@@ -102,6 +102,7 @@ export function logComponentRender(
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
const debugTask = componentInfo.debugTask;
|
||||
const measureName = '\u200b' + entryName;
|
||||
if (__DEV__ && debugTask) {
|
||||
const properties: Array<[string, string]> = [];
|
||||
if (componentInfo.key != null) {
|
||||
@@ -110,9 +111,10 @@ export function logComponentRender(
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0, '');
|
||||
}
|
||||
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, '\u200b' + entryName, {
|
||||
performance.measure.bind(performance, measureName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
@@ -125,9 +127,10 @@ export function logComponentRender(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
'\u200b' + entryName,
|
||||
measureName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
childrenEndTime,
|
||||
trackNames[trackIdx],
|
||||
@@ -152,6 +155,7 @@ export function logComponentAborted(
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
const measureName = '\u200b' + entryName;
|
||||
if (__DEV__) {
|
||||
const properties: Array<[string, string]> = [
|
||||
[
|
||||
@@ -165,7 +169,8 @@ export function logComponentAborted(
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0, '');
|
||||
}
|
||||
performance.measure('\u200b' + entryName, {
|
||||
|
||||
performance.measure(measureName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
@@ -178,9 +183,10 @@ export function logComponentAborted(
|
||||
},
|
||||
},
|
||||
});
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
measureName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
childrenEndTime,
|
||||
trackNames[trackIdx],
|
||||
@@ -206,6 +212,7 @@ export function logComponentErrored(
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
const measureName = '\u200b' + entryName;
|
||||
if (__DEV__) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
@@ -222,7 +229,8 @@ export function logComponentErrored(
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0, '');
|
||||
}
|
||||
performance.measure('\u200b' + entryName, {
|
||||
|
||||
performance.measure(measureName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
@@ -235,9 +243,10 @@ export function logComponentErrored(
|
||||
},
|
||||
},
|
||||
});
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
measureName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
childrenEndTime,
|
||||
trackNames[trackIdx],
|
||||
@@ -397,6 +406,7 @@ export function logComponentAwaitAborted(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(entryName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
@@ -453,6 +463,7 @@ export function logComponentAwaitErrored(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(entryName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
@@ -514,6 +525,7 @@ export function logComponentAwait(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(entryName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
@@ -538,6 +550,7 @@ export function logIOInfoErrored(
|
||||
const description = getIODescription(error);
|
||||
const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv);
|
||||
const debugTask = ioInfo.debugTask;
|
||||
const measureName = '\u200b' + entryName;
|
||||
if (__DEV__ && debugTask) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
@@ -550,9 +563,10 @@ export function logIOInfoErrored(
|
||||
const properties = [['rejected with', message]];
|
||||
const tooltipText =
|
||||
getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected';
|
||||
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, '\u200b' + entryName, {
|
||||
performance.measure.bind(performance, measureName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
@@ -565,9 +579,10 @@ export function logIOInfoErrored(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
measureName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
@@ -590,6 +605,7 @@ export function logIOInfo(
|
||||
const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv);
|
||||
const color = getIOColor(entryName);
|
||||
const debugTask = ioInfo.debugTask;
|
||||
const measureName = '\u200b' + entryName;
|
||||
if (__DEV__ && debugTask) {
|
||||
const properties: Array<[string, string]> = [];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
@@ -605,7 +621,7 @@ export function logIOInfo(
|
||||
);
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, '\u200b' + entryName, {
|
||||
performance.measure.bind(performance, measureName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
@@ -618,9 +634,10 @@ export function logIOInfo(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
measureName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-core",
|
||||
"version": "6.1.5",
|
||||
"version": "7.0.0",
|
||||
"description": "Use react-devtools outside of the browser",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"manifest_version": 3,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Chrome Developer Tools.",
|
||||
"version": "6.1.5",
|
||||
"version_name": "6.1.5",
|
||||
"version": "7.0.0",
|
||||
"version_name": "7.0.0",
|
||||
"minimum_chrome_version": "114",
|
||||
"icons": {
|
||||
"16": "icons/16-production.png",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"manifest_version": 3,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
|
||||
"version": "6.1.5",
|
||||
"version_name": "6.1.5",
|
||||
"version": "7.0.0",
|
||||
"version_name": "7.0.0",
|
||||
"minimum_chrome_version": "114",
|
||||
"icons": {
|
||||
"16": "icons/16-production.png",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Firefox Developer Tools.",
|
||||
"version": "6.1.5",
|
||||
"version": "7.0.0",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "@react-devtools",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-inline",
|
||||
"version": "6.1.5",
|
||||
"version": "7.0.0",
|
||||
"description": "Embed react-devtools within a website",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
|
||||
@@ -1546,7 +1546,7 @@ describe('Store', () => {
|
||||
▸ <Wrapper>
|
||||
`);
|
||||
|
||||
const deepestedNodeID = agent.getIDForHostInstance(ref.current);
|
||||
const deepestedNodeID = agent.getIDForHostInstance(ref.current).id;
|
||||
|
||||
await act(() => store.toggleIsCollapsed(deepestedNodeID, false));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
@@ -3142,4 +3142,105 @@ describe('Store', () => {
|
||||
await actAsync(() => render(null));
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19
|
||||
it('should keep suspended boundaries in the Suspense tree but not hidden Activity', async () => {
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
|
||||
const never = new Promise(() => {});
|
||||
function Never() {
|
||||
readValue(never);
|
||||
return null;
|
||||
}
|
||||
function Component({children}) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
function App({hidden}) {
|
||||
return (
|
||||
<>
|
||||
<Activity mode={hidden ? 'hidden' : 'visible'}>
|
||||
<React.Suspense name="inside-activity">
|
||||
<Component key="inside-activity">inside Activity</Component>
|
||||
</React.Suspense>
|
||||
</Activity>
|
||||
<React.Suspense name="outer-suspense">
|
||||
<React.Suspense name="inner-suspense">
|
||||
<Component key="inside-suspense">inside Suspense</Component>
|
||||
</React.Suspense>
|
||||
{hidden ? <Never /> : null}
|
||||
</React.Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(() => {
|
||||
render(<App hidden={true} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Activity>
|
||||
<Suspense name="outer-suspense">
|
||||
[suspense-root] rects={[{x:1,y:2,width:15,height:1}]}
|
||||
<Suspense name="outer-suspense" rects={null}>
|
||||
`);
|
||||
|
||||
// mount as visible
|
||||
await actAsync(() => {
|
||||
render(null);
|
||||
});
|
||||
await actAsync(() => {
|
||||
render(<App hidden={false} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Activity>
|
||||
▾ <Suspense name="inside-activity">
|
||||
<Component key="inside-activity">
|
||||
▾ <Suspense name="outer-suspense">
|
||||
▾ <Suspense name="inner-suspense">
|
||||
<Component key="inside-suspense">
|
||||
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
|
||||
<Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
render(<App hidden={true} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Activity>
|
||||
<Suspense name="outer-suspense">
|
||||
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
|
||||
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
render(<App hidden={false} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Activity>
|
||||
▾ <Suspense name="inside-activity">
|
||||
<Component key="inside-activity">
|
||||
▾ <Suspense name="outer-suspense">
|
||||
▾ <Suspense name="inner-suspense">
|
||||
<Component key="inside-suspense">
|
||||
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
|
||||
<Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -251,7 +251,8 @@ describe('Store component filters', () => {
|
||||
});
|
||||
|
||||
it('should filter ViewTransition', async () => {
|
||||
const ViewTransition = React.unstable_ViewTransition;
|
||||
const ViewTransition =
|
||||
React.ViewTransition || React.unstable_ViewTransition;
|
||||
|
||||
if (ViewTransition != null) {
|
||||
await actAsync(async () =>
|
||||
|
||||
100
packages/react-devtools-shared/src/backend/agent.js
vendored
100
packages/react-devtools-shared/src/backend/agent.js
vendored
@@ -455,7 +455,10 @@ export default class Agent extends EventEmitter<{
|
||||
return renderer.getInstanceAndStyle(id);
|
||||
}
|
||||
|
||||
getIDForHostInstance(target: HostInstance): number | null {
|
||||
getIDForHostInstance(
|
||||
target: HostInstance,
|
||||
onlySuspenseNodes?: boolean,
|
||||
): null | {id: number, rendererID: number} {
|
||||
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
|
||||
// In React Native or non-DOM we simply pick any renderer that has a match.
|
||||
for (const rendererID in this._rendererInterfaces) {
|
||||
@@ -463,9 +466,14 @@ export default class Agent extends EventEmitter<{
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
try {
|
||||
const match = renderer.getElementIDForHostInstance(target);
|
||||
if (match != null) {
|
||||
return match;
|
||||
const id = onlySuspenseNodes
|
||||
? renderer.getSuspenseNodeIDForHostInstance(target)
|
||||
: renderer.getElementIDForHostInstance(target);
|
||||
if (id !== null) {
|
||||
return {
|
||||
id: id,
|
||||
rendererID: +rendererID,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Some old React versions might throw if they can't find a match.
|
||||
@@ -478,6 +486,7 @@ export default class Agent extends EventEmitter<{
|
||||
// that is registered if there isn't an exact match.
|
||||
let bestMatch: null | Element = null;
|
||||
let bestRenderer: null | RendererInterface = null;
|
||||
let bestRendererID: number = 0;
|
||||
// Find the nearest ancestor which is mounted by a React.
|
||||
for (const rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
@@ -491,6 +500,7 @@ export default class Agent extends EventEmitter<{
|
||||
// Exact match we can exit early.
|
||||
bestMatch = nearestNode;
|
||||
bestRenderer = renderer;
|
||||
bestRendererID = +rendererID;
|
||||
break;
|
||||
}
|
||||
if (bestMatch === null || bestMatch.contains(nearestNode)) {
|
||||
@@ -498,12 +508,21 @@ export default class Agent extends EventEmitter<{
|
||||
// so the new match is a deeper and therefore better match.
|
||||
bestMatch = nearestNode;
|
||||
bestRenderer = renderer;
|
||||
bestRendererID = +rendererID;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestRenderer != null && bestMatch != null) {
|
||||
try {
|
||||
return bestRenderer.getElementIDForHostInstance(bestMatch);
|
||||
const id = onlySuspenseNodes
|
||||
? bestRenderer.getSuspenseNodeIDForHostInstance(bestMatch)
|
||||
: bestRenderer.getElementIDForHostInstance(bestMatch);
|
||||
if (id !== null) {
|
||||
return {
|
||||
id,
|
||||
rendererID: bestRendererID,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Some old React versions might throw if they can't find a match.
|
||||
// If so we should ignore it...
|
||||
@@ -514,65 +533,14 @@ export default class Agent extends EventEmitter<{
|
||||
}
|
||||
|
||||
getComponentNameForHostInstance(target: HostInstance): string | null {
|
||||
// We duplicate this code from getIDForHostInstance to avoid an object allocation.
|
||||
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
|
||||
// In React Native or non-DOM we simply pick any renderer that has a match.
|
||||
for (const rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
try {
|
||||
const id = renderer.getElementIDForHostInstance(target);
|
||||
if (id) {
|
||||
return renderer.getDisplayNameForElementID(id);
|
||||
}
|
||||
} catch (error) {
|
||||
// Some old React versions might throw if they can't find a match.
|
||||
// If so we should ignore it...
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
// In the DOM we use a smarter mechanism to find the deepest a DOM node
|
||||
// that is registered if there isn't an exact match.
|
||||
let bestMatch: null | Element = null;
|
||||
let bestRenderer: null | RendererInterface = null;
|
||||
// Find the nearest ancestor which is mounted by a React.
|
||||
for (const rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
const nearestNode: null | Element = renderer.getNearestMountedDOMNode(
|
||||
(target: any),
|
||||
);
|
||||
if (nearestNode !== null) {
|
||||
if (nearestNode === target) {
|
||||
// Exact match we can exit early.
|
||||
bestMatch = nearestNode;
|
||||
bestRenderer = renderer;
|
||||
break;
|
||||
}
|
||||
if (bestMatch === null || bestMatch.contains(nearestNode)) {
|
||||
// If this is the first match or the previous match contains the new match,
|
||||
// so the new match is a deeper and therefore better match.
|
||||
bestMatch = nearestNode;
|
||||
bestRenderer = renderer;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestRenderer != null && bestMatch != null) {
|
||||
try {
|
||||
const id = bestRenderer.getElementIDForHostInstance(bestMatch);
|
||||
if (id) {
|
||||
return bestRenderer.getDisplayNameForElementID(id);
|
||||
}
|
||||
} catch (error) {
|
||||
// Some old React versions might throw if they can't find a match.
|
||||
// If so we should ignore it...
|
||||
}
|
||||
}
|
||||
return null;
|
||||
const match = this.getIDForHostInstance(target);
|
||||
if (match !== null) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(match.rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
return renderer.getDisplayNameForElementID(match.id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getBackendVersion: () => void = () => {
|
||||
@@ -971,9 +939,9 @@ export default class Agent extends EventEmitter<{
|
||||
};
|
||||
|
||||
selectNode(target: HostInstance): void {
|
||||
const id = this.getIDForHostInstance(target);
|
||||
if (id !== null) {
|
||||
this._bridge.send('selectElement', id);
|
||||
const match = this.getIDForHostInstance(target);
|
||||
if (match !== null) {
|
||||
this._bridge.send('selectElement', match.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2139,8 +2139,8 @@ export function attach(
|
||||
// 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),
|
||||
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, isSuspended]]
|
||||
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 3 : 0),
|
||||
);
|
||||
|
||||
// Identify which renderer this update is coming from.
|
||||
@@ -2225,6 +2225,14 @@ export function attach(
|
||||
}
|
||||
operations[i++] = fiberIdWithChanges;
|
||||
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
|
||||
const instance = suspense.instance;
|
||||
const isSuspended =
|
||||
// TODO: Track if other SuspenseNode like SuspenseList rows are suspended.
|
||||
(instance.kind === FIBER_INSTANCE ||
|
||||
instance.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
instance.data.tag === SuspenseComponent &&
|
||||
instance.data.memoizedState !== null;
|
||||
operations[i++] = isSuspended ? 1 : 0;
|
||||
operations[i++] = suspense.environments.size;
|
||||
suspense.environments.forEach((count, env) => {
|
||||
operations[i++] = getStringID(env);
|
||||
@@ -2251,7 +2259,10 @@ export function attach(
|
||||
if (typeof instance !== 'object' || instance === null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof instance.getClientRects === 'function') {
|
||||
if (
|
||||
typeof instance.getClientRects === 'function' ||
|
||||
instance.nodeType === 3
|
||||
) {
|
||||
// DOM
|
||||
const doc = instance.ownerDocument;
|
||||
if (instance === doc.documentElement) {
|
||||
@@ -2273,7 +2284,21 @@ export function attach(
|
||||
const win = doc && doc.defaultView;
|
||||
const scrollX = win ? win.scrollX : 0;
|
||||
const scrollY = win ? win.scrollY : 0;
|
||||
const rects = instance.getClientRects();
|
||||
let rects;
|
||||
if (instance.nodeType === 3) {
|
||||
// Text nodes cannot be measured directly but we can measure a Range.
|
||||
if (typeof doc.createRange !== 'function') {
|
||||
return null;
|
||||
}
|
||||
const range = doc.createRange();
|
||||
if (typeof range.getClientRects !== 'function') {
|
||||
return null;
|
||||
}
|
||||
range.selectNodeContents(instance);
|
||||
rects = range.getClientRects();
|
||||
} else {
|
||||
rects = instance.getClientRects();
|
||||
}
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
const rect = rects[i];
|
||||
result.push({
|
||||
@@ -2640,9 +2665,15 @@ export function attach(
|
||||
const fiber = fiberInstance.data;
|
||||
const props = fiber.memoizedProps;
|
||||
// TODO: Compute a fallback name based on Owner, key etc.
|
||||
const name = props === null ? null : props.name || null;
|
||||
const name =
|
||||
fiber.tag !== SuspenseComponent || props === null
|
||||
? null
|
||||
: props.name || null;
|
||||
const nameStringID = getStringID(name);
|
||||
|
||||
const isSuspended =
|
||||
fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
|
||||
|
||||
if (__DEBUG__) {
|
||||
console.log('recordSuspenseMount()', suspenseInstance);
|
||||
}
|
||||
@@ -2653,6 +2684,7 @@ export function attach(
|
||||
pushOperation(fiberID);
|
||||
pushOperation(parentID);
|
||||
pushOperation(nameStringID);
|
||||
pushOperation(isSuspended ? 1 : 0);
|
||||
|
||||
const rects = suspenseInstance.rects;
|
||||
if (rects === null) {
|
||||
@@ -2661,10 +2693,10 @@ export function attach(
|
||||
pushOperation(rects.length);
|
||||
for (let i = 0; i < rects.length; ++i) {
|
||||
const rect = rects[i];
|
||||
pushOperation(Math.round(rect.x));
|
||||
pushOperation(Math.round(rect.y));
|
||||
pushOperation(Math.round(rect.width));
|
||||
pushOperation(Math.round(rect.height));
|
||||
pushOperation(Math.round(rect.x * 1000));
|
||||
pushOperation(Math.round(rect.y * 1000));
|
||||
pushOperation(Math.round(rect.width * 1000));
|
||||
pushOperation(Math.round(rect.height * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2733,10 +2765,10 @@ export function attach(
|
||||
pushOperation(rects.length);
|
||||
for (let i = 0; i < rects.length; ++i) {
|
||||
const rect = rects[i];
|
||||
pushOperation(Math.round(rect.x));
|
||||
pushOperation(Math.round(rect.y));
|
||||
pushOperation(Math.round(rect.width));
|
||||
pushOperation(Math.round(rect.height));
|
||||
pushOperation(Math.round(rect.x * 1000));
|
||||
pushOperation(Math.round(rect.y * 1000));
|
||||
pushOperation(Math.round(rect.width * 1000));
|
||||
pushOperation(Math.round(rect.height * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3144,12 +3176,30 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offscreen of suspended Suspense
|
||||
*/
|
||||
function isSuspendedOffscreen(fiber: Fiber): boolean {
|
||||
switch (fiber.tag) {
|
||||
case LegacyHiddenComponent:
|
||||
// fallthrough since all published implementations currently implement the same state as Offscreen.
|
||||
case OffscreenComponent:
|
||||
return (
|
||||
fiber.memoizedState !== null &&
|
||||
fiber.return !== null &&
|
||||
fiber.return.tag === SuspenseComponent
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function unmountRemainingChildren() {
|
||||
if (
|
||||
reconcilingParent !== null &&
|
||||
(reconcilingParent.kind === FIBER_INSTANCE ||
|
||||
reconcilingParent.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
isHiddenOffscreen(reconcilingParent.data) &&
|
||||
isSuspendedOffscreen(reconcilingParent.data) &&
|
||||
!isInDisconnectedSubtree
|
||||
) {
|
||||
// This is a hidden offscreen, we need to execute this in the context of a disconnected subtree.
|
||||
@@ -3244,14 +3294,22 @@ export function attach(
|
||||
// We don't update rects inside disconnected subtrees.
|
||||
return;
|
||||
}
|
||||
const nextRects = measureInstance(suspenseNode.instance);
|
||||
const prevRects = suspenseNode.rects;
|
||||
if (areEqualRects(prevRects, nextRects)) {
|
||||
return; // Unchanged
|
||||
const instance = suspenseNode.instance;
|
||||
|
||||
const isSuspendedSuspenseComponent =
|
||||
(instance.kind === FIBER_INSTANCE ||
|
||||
instance.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
instance.data.tag === SuspenseComponent &&
|
||||
instance.data.memoizedState !== null;
|
||||
if (isSuspendedSuspenseComponent) {
|
||||
// This boundary itself was suspended and we don't measure those since that would measure
|
||||
// the fallback. We want to keep a ghost of the rectangle of the content not currently shown.
|
||||
return;
|
||||
}
|
||||
// The rect has changed. While the bailed out root wasn't in a disconnected subtree,
|
||||
|
||||
// While this boundary wasn't suspended and the bailed out root and wasn't in a disconnected subtree,
|
||||
// it's possible that this node was in one. So we need to check if we're offscreen.
|
||||
let parent = suspenseNode.instance.parent;
|
||||
let parent = instance.parent;
|
||||
while (parent !== null) {
|
||||
if (
|
||||
(parent.kind === FIBER_INSTANCE ||
|
||||
@@ -3267,6 +3325,13 @@ export function attach(
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
const nextRects = measureInstance(suspenseNode.instance);
|
||||
const prevRects = suspenseNode.rects;
|
||||
if (areEqualRects(prevRects, nextRects)) {
|
||||
return; // Unchanged
|
||||
}
|
||||
|
||||
// We changed inside a visible tree.
|
||||
// Since this boundary changed, it's possible it also affected its children so lets
|
||||
// measure them as well.
|
||||
@@ -4026,7 +4091,7 @@ export function attach(
|
||||
trackDebugInfoFromHostComponent(nearestInstance, fiber);
|
||||
}
|
||||
|
||||
if (isHiddenOffscreen(fiber)) {
|
||||
if (isSuspendedOffscreen(fiber)) {
|
||||
// If an Offscreen component is hidden, mount its children as disconnected.
|
||||
const stashedDisconnected = isInDisconnectedSubtree;
|
||||
isInDisconnectedSubtree = true;
|
||||
@@ -4037,6 +4102,9 @@ export function attach(
|
||||
} finally {
|
||||
isInDisconnectedSubtree = stashedDisconnected;
|
||||
}
|
||||
} else if (isHiddenOffscreen(fiber)) {
|
||||
// hidden Activity is noisy.
|
||||
// Including it may show overlapping Suspense rects
|
||||
} else if (fiber.tag === SuspenseComponent && OffscreenComponent === -1) {
|
||||
// Legacy Suspense without the Offscreen wrapper. For the modern Suspense we just handle the
|
||||
// Offscreen wrapper itself specially.
|
||||
@@ -4981,17 +5049,28 @@ export function attach(
|
||||
|
||||
const prevWasHidden = isHiddenOffscreen(prevFiber);
|
||||
const nextIsHidden = isHiddenOffscreen(nextFiber);
|
||||
const prevWasSuspended = isSuspendedOffscreen(prevFiber);
|
||||
const nextIsSuspended = isSuspendedOffscreen(nextFiber);
|
||||
|
||||
if (isLegacySuspense) {
|
||||
if (
|
||||
fiberInstance !== null &&
|
||||
fiberInstance.suspenseNode !== null &&
|
||||
(prevFiber.stateNode === null) !== (nextFiber.stateNode === null)
|
||||
) {
|
||||
trackThrownPromisesFromRetryCache(
|
||||
fiberInstance.suspenseNode,
|
||||
nextFiber.stateNode,
|
||||
);
|
||||
if (fiberInstance !== null && fiberInstance.suspenseNode !== null) {
|
||||
const suspenseNode = fiberInstance.suspenseNode;
|
||||
if (
|
||||
(prevFiber.stateNode === null) !==
|
||||
(nextFiber.stateNode === null)
|
||||
) {
|
||||
trackThrownPromisesFromRetryCache(
|
||||
suspenseNode,
|
||||
nextFiber.stateNode,
|
||||
);
|
||||
}
|
||||
if (
|
||||
(prevFiber.memoizedState === null) !==
|
||||
(nextFiber.memoizedState === null)
|
||||
) {
|
||||
// Toggle suspended state.
|
||||
recordSuspenseSuspenders(suspenseNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The logic below is inspired by the code paths in updateSuspenseComponent()
|
||||
@@ -5058,8 +5137,8 @@ export function attach(
|
||||
);
|
||||
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
|
||||
}
|
||||
} else if (nextIsHidden) {
|
||||
if (!prevWasHidden) {
|
||||
} else if (nextIsSuspended) {
|
||||
if (!prevWasSuspended) {
|
||||
// We're hiding the children. Disconnect them from the front end but keep state.
|
||||
if (fiberInstance !== null && !isInDisconnectedSubtree) {
|
||||
disconnectChildrenRecursively(remainingReconcilingChildren);
|
||||
@@ -5077,7 +5156,7 @@ export function attach(
|
||||
} finally {
|
||||
isInDisconnectedSubtree = stashedDisconnected;
|
||||
}
|
||||
} else if (prevWasHidden && !nextIsHidden) {
|
||||
} else if (prevWasSuspended && !nextIsSuspended) {
|
||||
// We're revealing the hidden children. We now need to update them to the latest state.
|
||||
// We do this while still in the disconnected state and then we reconnect the new ones.
|
||||
// This avoids reconnecting things that are about to be removed anyway.
|
||||
@@ -5103,6 +5182,13 @@ export function attach(
|
||||
// Children may have reordered while they were hidden.
|
||||
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
|
||||
}
|
||||
} else if (nextIsHidden) {
|
||||
if (prevWasHidden) {
|
||||
// still hidden. Nothing to do.
|
||||
} else {
|
||||
// We're hiding the children. Remove them from the Frontend
|
||||
unmountRemainingChildren();
|
||||
}
|
||||
} else if (
|
||||
nextFiber.tag === SuspenseComponent &&
|
||||
OffscreenComponent !== -1 &&
|
||||
@@ -5132,6 +5218,14 @@ export function attach(
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(prevFiber.memoizedState === null) !==
|
||||
(nextFiber.memoizedState === null)
|
||||
) {
|
||||
// Toggle suspended state.
|
||||
recordSuspenseSuspenders(suspenseNode);
|
||||
}
|
||||
|
||||
shouldMeasureSuspenseNode = false;
|
||||
updateFlags |= updateSuspenseChildrenRecursively(
|
||||
nextContentFiber,
|
||||
@@ -5158,6 +5252,8 @@ export function attach(
|
||||
}
|
||||
|
||||
trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode);
|
||||
// Toggle suspended state.
|
||||
recordSuspenseSuspenders(suspenseNode);
|
||||
|
||||
mountSuspenseChildrenRecursively(
|
||||
nextContentFiber,
|
||||
@@ -5259,7 +5355,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) {
|
||||
if (!nextIsSuspended && !isInDisconnectedSubtree) {
|
||||
recordResetChildren(fiberInstance);
|
||||
}
|
||||
|
||||
@@ -5335,7 +5431,7 @@ export function attach(
|
||||
if (
|
||||
(child.kind === FIBER_INSTANCE ||
|
||||
child.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
isHiddenOffscreen(child.data)
|
||||
isSuspendedOffscreen(child.data)
|
||||
) {
|
||||
// This instance's children are already disconnected.
|
||||
} else {
|
||||
@@ -5697,7 +5793,28 @@ export function attach(
|
||||
return null;
|
||||
}
|
||||
if (devtoolsInstance.kind === FIBER_INSTANCE) {
|
||||
return getDisplayNameForFiber(devtoolsInstance.data);
|
||||
const fiber = devtoolsInstance.data;
|
||||
if (fiber.tag === HostRoot) {
|
||||
// The only reason you'd inspect a HostRoot is to show it as a SuspenseNode.
|
||||
return 'Initial Paint';
|
||||
}
|
||||
if (fiber.tag === SuspenseComponent || fiber.tag === ActivityComponent) {
|
||||
// For Suspense and Activity components, we can show a better name
|
||||
// by using the name prop or their owner.
|
||||
const props = fiber.memoizedProps;
|
||||
if (props.name != null) {
|
||||
return props.name;
|
||||
}
|
||||
const owner = getUnfilteredOwner(fiber);
|
||||
if (owner != null) {
|
||||
if (typeof owner.tag === 'number') {
|
||||
return getDisplayNameForFiber((owner: any));
|
||||
} else {
|
||||
return owner.name || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return getDisplayNameForFiber(fiber);
|
||||
} else {
|
||||
return devtoolsInstance.data.name || '';
|
||||
}
|
||||
@@ -5738,6 +5855,28 @@ export function attach(
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSuspenseNodeIDForHostInstance(
|
||||
publicInstance: HostInstance,
|
||||
): number | null {
|
||||
const instance = publicInstanceToDevToolsInstanceMap.get(publicInstance);
|
||||
if (instance !== undefined) {
|
||||
// Pick nearest unfiltered SuspenseNode instance.
|
||||
let suspenseInstance = instance;
|
||||
while (
|
||||
suspenseInstance.suspenseNode === null ||
|
||||
suspenseInstance.kind === FILTERED_FIBER_INSTANCE
|
||||
) {
|
||||
if (suspenseInstance.parent === null) {
|
||||
// We shouldn't get here since we'll always have a suspenseNode at the root.
|
||||
return null;
|
||||
}
|
||||
suspenseInstance = suspenseInstance.parent;
|
||||
}
|
||||
return suspenseInstance.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getElementAttributeByPath(
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
@@ -8534,6 +8673,7 @@ export function attach(
|
||||
getDisplayNameForElementID,
|
||||
getNearestMountedDOMNode,
|
||||
getElementIDForHostInstance,
|
||||
getSuspenseNodeIDForHostInstance,
|
||||
getInstanceAndStyle,
|
||||
getOwnersList,
|
||||
getPathForElement,
|
||||
|
||||
@@ -169,6 +169,9 @@ export function attach(
|
||||
getElementIDForHostInstance() {
|
||||
return null;
|
||||
},
|
||||
getSuspenseNodeIDForHostInstance() {
|
||||
return null;
|
||||
},
|
||||
getInstanceAndStyle() {
|
||||
return {
|
||||
instance: null,
|
||||
|
||||
@@ -417,6 +417,7 @@ export function attach(
|
||||
pushOperation(id);
|
||||
pushOperation(parentID);
|
||||
pushOperation(getStringID(null)); // name
|
||||
pushOperation(0); // isSuspended
|
||||
// TODO: Measure rect of root
|
||||
pushOperation(-1);
|
||||
} else {
|
||||
@@ -1268,6 +1269,9 @@ export function attach(
|
||||
getDisplayNameForElementID,
|
||||
getNearestMountedDOMNode,
|
||||
getElementIDForHostInstance,
|
||||
getSuspenseNodeIDForHostInstance(id: number): null {
|
||||
return null;
|
||||
},
|
||||
getInstanceAndStyle,
|
||||
findHostInstancesForElementID: (id: number) => {
|
||||
const hostInstance = findHostInstanceForInternalID(id);
|
||||
|
||||
@@ -427,6 +427,7 @@ export type RendererInterface = {
|
||||
getComponentStack?: GetComponentStack,
|
||||
getNearestMountedDOMNode: (component: Element) => Element | null,
|
||||
getElementIDForHostInstance: GetElementIDForHostInstance,
|
||||
getSuspenseNodeIDForHostInstance: GetElementIDForHostInstance,
|
||||
getDisplayNameForElementID: GetDisplayNameForElementID,
|
||||
getInstanceAndStyle(id: number): InstanceAndStyle,
|
||||
getProfilingData(): ProfilingDataBackend,
|
||||
|
||||
@@ -187,10 +187,13 @@ export default class Overlay {
|
||||
}
|
||||
}
|
||||
|
||||
inspect(nodes: $ReadOnlyArray<HTMLElement>, name?: ?string) {
|
||||
inspect(nodes: $ReadOnlyArray<HTMLElement | Text>, name?: ?string) {
|
||||
// We can't get the size of text nodes or comment nodes. React as of v15
|
||||
// heavily uses comment nodes to delimit text.
|
||||
const elements = nodes.filter(node => node.nodeType === Node.ELEMENT_NODE);
|
||||
// TODO: We actually can measure text nodes. We should.
|
||||
const elements: $ReadOnlyArray<HTMLElement> = (nodes.filter(
|
||||
node => node.nodeType === Node.ELEMENT_NODE,
|
||||
): any);
|
||||
|
||||
while (this.rects.length > elements.length) {
|
||||
const rect = this.rects.pop();
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {RendererInterface} from '../../types';
|
||||
// That is done by the React Native Inspector component.
|
||||
|
||||
let iframesListeningTo: Set<HTMLIFrameElement> = new Set();
|
||||
let inspectOnlySuspenseNodes = false;
|
||||
|
||||
export default function setupHighlighter(
|
||||
bridge: BackendBridge,
|
||||
@@ -33,7 +34,8 @@ export default function setupHighlighter(
|
||||
bridge.addListener('startInspectingHost', startInspectingHost);
|
||||
bridge.addListener('stopInspectingHost', stopInspectingHost);
|
||||
|
||||
function startInspectingHost() {
|
||||
function startInspectingHost(onlySuspenseNodes: boolean) {
|
||||
inspectOnlySuspenseNodes = onlySuspenseNodes;
|
||||
registerListenersOnWindow(window);
|
||||
}
|
||||
|
||||
@@ -363,11 +365,37 @@ export default function setupHighlighter(
|
||||
}
|
||||
}
|
||||
|
||||
// Don't pass the name explicitly.
|
||||
// It will be inferred from DOM tag and Fiber owner.
|
||||
showOverlay([target], null, agent, false);
|
||||
|
||||
selectElementForNode(target);
|
||||
if (inspectOnlySuspenseNodes) {
|
||||
// For Suspense nodes we want to highlight not the actual target but the nodes
|
||||
// that are the root of the Suspense node.
|
||||
// TODO: Consider if we should just do the same for other elements because the
|
||||
// hovered node might just be one child of many in the Component.
|
||||
const match = agent.getIDForHostInstance(
|
||||
target,
|
||||
inspectOnlySuspenseNodes,
|
||||
);
|
||||
if (match !== null) {
|
||||
const renderer = agent.rendererInterfaces[match.rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(
|
||||
`Invalid renderer id "${match.rendererID}" for element "${match.id}"`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
highlightHostInstance({
|
||||
displayName: renderer.getDisplayNameForElementID(match.id),
|
||||
hideAfterTimeout: false,
|
||||
id: match.id,
|
||||
openBuiltinElementsPanel: false,
|
||||
rendererID: match.rendererID,
|
||||
scrollIntoView: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Don't pass the name explicitly.
|
||||
// It will be inferred from DOM tag and Fiber owner.
|
||||
showOverlay([target], null, agent, false);
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp(event: MouseEvent) {
|
||||
@@ -376,9 +404,9 @@ export default function setupHighlighter(
|
||||
}
|
||||
|
||||
const selectElementForNode = (node: HTMLElement) => {
|
||||
const id = agent.getIDForHostInstance(node);
|
||||
if (id !== null) {
|
||||
bridge.send('selectElement', id);
|
||||
const match = agent.getIDForHostInstance(node, inspectOnlySuspenseNodes);
|
||||
if (match !== null) {
|
||||
bridge.send('selectElement', match.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
23
packages/react-devtools-shared/src/bridge.js
vendored
23
packages/react-devtools-shared/src/bridge.js
vendored
@@ -217,10 +217,15 @@ export type BackendEvents = {
|
||||
selectElement: [number],
|
||||
shutdown: [],
|
||||
stopInspectingHost: [boolean],
|
||||
syncSelectionFromBuiltinElementsPanel: [],
|
||||
syncSelectionToBuiltinElementsPanel: [],
|
||||
unsupportedRendererVersion: [],
|
||||
|
||||
extensionComponentsPanelShown: [],
|
||||
extensionComponentsPanelHidden: [],
|
||||
|
||||
resumeElementPolling: [],
|
||||
pauseElementPolling: [],
|
||||
|
||||
// React Native style editor plug-in.
|
||||
isNativeStyleEditorSupported: [
|
||||
{isSupported: boolean, validAttributes: ?$ReadOnlyArray<string>},
|
||||
@@ -240,8 +245,6 @@ type FrontendEvents = {
|
||||
clearWarningsForElementID: [ElementAndRendererID],
|
||||
copyElementPath: [CopyElementPathParams],
|
||||
deletePath: [DeletePath],
|
||||
extensionComponentsPanelShown: [],
|
||||
extensionComponentsPanelHidden: [],
|
||||
getBackendVersion: [],
|
||||
getBridgeProtocol: [],
|
||||
getIfHasUnsupportedRendererVersion: [],
|
||||
@@ -263,9 +266,9 @@ type FrontendEvents = {
|
||||
savedPreferences: [SavedPreferencesParams],
|
||||
setTraceUpdatesEnabled: [boolean],
|
||||
shutdown: [],
|
||||
startInspectingHost: [],
|
||||
startInspectingHost: [boolean],
|
||||
startProfiling: [StartProfilingParams],
|
||||
stopInspectingHost: [boolean],
|
||||
stopInspectingHost: [],
|
||||
scrollToHostInstance: [ScrollToHostInstance],
|
||||
stopProfiling: [],
|
||||
storeAsGlobal: [StoreAsGlobalParams],
|
||||
@@ -275,6 +278,8 @@ type FrontendEvents = {
|
||||
viewAttributeSource: [ViewAttributeSourceParams],
|
||||
viewElementSource: [ElementAndRendererID],
|
||||
|
||||
syncSelectionFromBuiltinElementsPanel: [],
|
||||
|
||||
// React Native style editor plug-in.
|
||||
NativeStyleEditor_measure: [ElementAndRendererID],
|
||||
NativeStyleEditor_renameAttribute: [NativeStyleEditor_RenameAttributeParams],
|
||||
@@ -295,19 +300,13 @@ type FrontendEvents = {
|
||||
overrideProps: [OverrideValue],
|
||||
overrideState: [OverrideValue],
|
||||
|
||||
resumeElementPolling: [],
|
||||
pauseElementPolling: [],
|
||||
|
||||
getHookSettings: [],
|
||||
};
|
||||
|
||||
class Bridge<
|
||||
OutgoingEvents: Object,
|
||||
IncomingEvents: Object,
|
||||
> extends EventEmitter<{
|
||||
...IncomingEvents,
|
||||
...OutgoingEvents,
|
||||
}> {
|
||||
> extends EventEmitter<IncomingEvents> {
|
||||
_isShutdown: boolean = false;
|
||||
_messageQueue: Array<any> = [];
|
||||
_scheduledFlush: boolean = false;
|
||||
|
||||
@@ -51,6 +51,7 @@ import type {
|
||||
ComponentFilter,
|
||||
ElementType,
|
||||
SuspenseNode,
|
||||
Rect,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {
|
||||
FrontendBridge,
|
||||
@@ -99,6 +100,10 @@ export type Capabilities = {
|
||||
supportsAdvancedProfiling: AdvancedProfiling,
|
||||
};
|
||||
|
||||
function isNonZeroRect(rect: Rect) {
|
||||
return rect.width > 0 || rect.height > 0 || rect.x > 0 || rect.y > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The store is the single source of truth for updates from the backend.
|
||||
* ContextProviders can subscribe to the Store for specific things they want to provide.
|
||||
@@ -918,7 +923,15 @@ export default class Store extends EventEmitter<{
|
||||
if (current === undefined) {
|
||||
continue;
|
||||
}
|
||||
// Ignore any suspense boundaries that has no visual representation as this is not
|
||||
// part of the visible loading sequence.
|
||||
// TODO: Consider making visible meta data and other side-effects get virtual rects.
|
||||
const hasRects =
|
||||
current.rects !== null &&
|
||||
current.rects.length > 0 &&
|
||||
current.rects.some(isNonZeroRect);
|
||||
if (
|
||||
hasRects &&
|
||||
(!uniqueSuspendersOnly || current.hasUniqueSuspenders) &&
|
||||
// Roots are already included as part of the Screen
|
||||
current.id !== rootID
|
||||
@@ -1539,7 +1552,8 @@ export default class Store extends EventEmitter<{
|
||||
const id = operations[i + 1];
|
||||
const parentID = operations[i + 2];
|
||||
const nameStringID = operations[i + 3];
|
||||
const numRects = ((operations[i + 4]: any): number);
|
||||
const isSuspended = operations[i + 4] === 1;
|
||||
const numRects = ((operations[i + 5]: any): number);
|
||||
let name = stringTable[nameStringID];
|
||||
|
||||
if (this._idToSuspense.has(id)) {
|
||||
@@ -1566,17 +1580,17 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
}
|
||||
|
||||
i += 5;
|
||||
i += 6;
|
||||
let rects: SuspenseNode['rects'];
|
||||
if (numRects === -1) {
|
||||
rects = null;
|
||||
} else {
|
||||
rects = [];
|
||||
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
|
||||
const x = operations[i + 0];
|
||||
const y = operations[i + 1];
|
||||
const width = operations[i + 2];
|
||||
const height = operations[i + 3];
|
||||
const x = operations[i + 0] / 1000;
|
||||
const y = operations[i + 1] / 1000;
|
||||
const width = operations[i + 2] / 1000;
|
||||
const height = operations[i + 3] / 1000;
|
||||
rects.push({x, y, width, height});
|
||||
i += 4;
|
||||
}
|
||||
@@ -1612,6 +1626,7 @@ export default class Store extends EventEmitter<{
|
||||
name,
|
||||
rects,
|
||||
hasUniqueSuspenders: false,
|
||||
isSuspended: isSuspended,
|
||||
});
|
||||
|
||||
hasSuspenseTreeChanged = true;
|
||||
@@ -1748,10 +1763,10 @@ export default class Store extends EventEmitter<{
|
||||
} else {
|
||||
nextRects = [];
|
||||
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
|
||||
const x = operations[i + 0];
|
||||
const y = operations[i + 1];
|
||||
const width = operations[i + 2];
|
||||
const height = operations[i + 3];
|
||||
const x = operations[i + 0] / 1000;
|
||||
const y = operations[i + 1] / 1000;
|
||||
const width = operations[i + 2] / 1000;
|
||||
const height = operations[i + 3] / 1000;
|
||||
|
||||
nextRects.push({x, y, width, height});
|
||||
|
||||
@@ -1788,6 +1803,7 @@ export default class Store extends EventEmitter<{
|
||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||
const id = operations[i++];
|
||||
const hasUniqueSuspenders = operations[i++] === 1;
|
||||
const isSuspended = operations[i++] === 1;
|
||||
const environmentNamesLength = operations[i++];
|
||||
const environmentNames = [];
|
||||
for (
|
||||
@@ -1819,6 +1835,7 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
|
||||
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
|
||||
suspense.isSuspended = isSuspended;
|
||||
// TODO: Recompute the environment names.
|
||||
}
|
||||
|
||||
|
||||
@@ -181,9 +181,7 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
className={styles.KeyValue}
|
||||
title={key}
|
||||
onDoubleClick={handleKeyDoubleClick}>
|
||||
<pre>
|
||||
<IndexableDisplayName displayName={key} id={id} />
|
||||
</pre>
|
||||
<IndexableDisplayName displayName={key} id={id} />
|
||||
</span>
|
||||
"
|
||||
</Fragment>
|
||||
@@ -196,9 +194,7 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
className={styles.KeyValue}
|
||||
title={nameProp}
|
||||
onDoubleClick={handleKeyDoubleClick}>
|
||||
<pre>
|
||||
<IndexableDisplayName displayName={nameProp} id={id} />
|
||||
</pre>
|
||||
<IndexableDisplayName displayName={nameProp} id={id} />
|
||||
</span>
|
||||
"
|
||||
</Fragment>
|
||||
|
||||
@@ -14,7 +14,11 @@ import Toggle from '../Toggle';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||
|
||||
export default function InspectHostNodesToggle(): React.Node {
|
||||
export default function InspectHostNodesToggle({
|
||||
onlySuspenseNodes,
|
||||
}: {
|
||||
onlySuspenseNodes?: boolean,
|
||||
}): React.Node {
|
||||
const [isInspecting, setIsInspecting] = useState(false);
|
||||
const bridge = useContext(BridgeContext);
|
||||
|
||||
@@ -24,9 +28,9 @@ export default function InspectHostNodesToggle(): React.Node {
|
||||
|
||||
if (isChecked) {
|
||||
logEvent({event_name: 'inspect-element-button-clicked'});
|
||||
bridge.send('startInspectingHost');
|
||||
bridge.send('startInspectingHost', !!onlySuspenseNodes);
|
||||
} else {
|
||||
bridge.send('stopInspectingHost', false);
|
||||
bridge.send('stopInspectingHost');
|
||||
}
|
||||
},
|
||||
[bridge],
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
}
|
||||
|
||||
let strictModeBadge = null;
|
||||
if (element.isStrictModeNonCompliant) {
|
||||
if (element.isStrictModeNonCompliant && element.parentID !== 0) {
|
||||
strictModeBadge = (
|
||||
<Tooltip label="This component is not running in StrictMode. Click to learn more.">
|
||||
<a
|
||||
@@ -237,7 +237,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
<div className={styles.SelectedComponentName}>
|
||||
<div
|
||||
className={
|
||||
element.isStrictModeNonCompliant
|
||||
element.isStrictModeNonCompliant && element.parentID !== 0
|
||||
? `${styles.ComponentName} ${styles.StrictModeNonCompliantComponentName}`
|
||||
: styles.ComponentName
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {useState, useTransition} from 'react';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import KeyValue from './KeyValue';
|
||||
import {serializeDataForCopy} from '../utils';
|
||||
import {serializeDataForCopy, pluralize} from '../utils';
|
||||
import Store from '../../store';
|
||||
import styles from './InspectedElementSharedStyles.css';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
@@ -44,6 +44,7 @@ type RowProps = {
|
||||
index: number,
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
skipName?: boolean,
|
||||
};
|
||||
|
||||
function getShortDescription(name: string, description: string): string {
|
||||
@@ -99,6 +100,7 @@ function SuspendedByRow({
|
||||
index,
|
||||
minTime,
|
||||
maxTime,
|
||||
skipName,
|
||||
}: RowProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [openIsPending, startOpenTransition] = useTransition();
|
||||
@@ -166,8 +168,10 @@ function SuspendedByRow({
|
||||
className={styles.CollapsableHeaderIcon}
|
||||
type={isOpen ? 'expanded' : 'collapsed'}
|
||||
/>
|
||||
<span className={styles.CollapsableHeaderTitle}>{name}</span>
|
||||
{shortDescription === '' ? null : (
|
||||
<span className={styles.CollapsableHeaderTitle}>
|
||||
{skipName ? shortDescription : name}
|
||||
</span>
|
||||
{skipName || shortDescription === '' ? null : (
|
||||
<>
|
||||
<span className={styles.CollapsableHeaderSeparator}>{' ('}</span>
|
||||
<span className={styles.CollapsableHeaderTitle}>
|
||||
@@ -300,15 +304,141 @@ type Props = {
|
||||
store: Store,
|
||||
};
|
||||
|
||||
function compareTime(a: SerializedAsyncInfo, b: SerializedAsyncInfo): number {
|
||||
const ioA = a.awaited;
|
||||
const ioB = b.awaited;
|
||||
function withIndex(
|
||||
value: SerializedAsyncInfo,
|
||||
index: number,
|
||||
): {
|
||||
index: number,
|
||||
value: SerializedAsyncInfo,
|
||||
} {
|
||||
return {
|
||||
index,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
function compareTime(
|
||||
a: {
|
||||
index: number,
|
||||
value: SerializedAsyncInfo,
|
||||
},
|
||||
b: {
|
||||
index: number,
|
||||
value: SerializedAsyncInfo,
|
||||
},
|
||||
): number {
|
||||
const ioA = a.value.awaited;
|
||||
const ioB = b.value.awaited;
|
||||
if (ioA.start === ioB.start) {
|
||||
return ioA.end - ioB.end;
|
||||
}
|
||||
return ioA.start - ioB.start;
|
||||
}
|
||||
|
||||
type GroupProps = {
|
||||
bridge: FrontendBridge,
|
||||
element: Element,
|
||||
inspectedElement: InspectedElement,
|
||||
store: Store,
|
||||
name: string,
|
||||
suspendedBy: Array<{
|
||||
index: number,
|
||||
value: SerializedAsyncInfo,
|
||||
}>,
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
};
|
||||
|
||||
function SuspendedByGroup({
|
||||
bridge,
|
||||
element,
|
||||
inspectedElement,
|
||||
store,
|
||||
name,
|
||||
suspendedBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
}: GroupProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
let start = Infinity;
|
||||
let end = -Infinity;
|
||||
let isRejected = false;
|
||||
for (let i = 0; i < suspendedBy.length; i++) {
|
||||
const asyncInfo: SerializedAsyncInfo = suspendedBy[i].value;
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
if (ioInfo.start < start) {
|
||||
start = ioInfo.start;
|
||||
}
|
||||
if (ioInfo.end > end) {
|
||||
end = ioInfo.end;
|
||||
}
|
||||
const value: any = ioInfo.value;
|
||||
if (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
value[meta.name] === 'rejected Thenable'
|
||||
) {
|
||||
isRejected = true;
|
||||
}
|
||||
}
|
||||
const timeScale = 100 / (maxTime - minTime);
|
||||
let left = (start - minTime) * timeScale;
|
||||
let width = (end - start) * timeScale;
|
||||
if (width < 5) {
|
||||
// Use at least a 5% width to avoid showing too small indicators.
|
||||
width = 5;
|
||||
if (left > 95) {
|
||||
left = 95;
|
||||
}
|
||||
}
|
||||
const pluralizedName = pluralize(name);
|
||||
return (
|
||||
<div className={styles.CollapsableRow}>
|
||||
<Button
|
||||
className={styles.CollapsableHeader}
|
||||
onClick={() => {
|
||||
setIsOpen(prevIsOpen => !prevIsOpen);
|
||||
}}
|
||||
title={pluralizedName}>
|
||||
<ButtonIcon
|
||||
className={styles.CollapsableHeaderIcon}
|
||||
type={isOpen ? 'expanded' : 'collapsed'}
|
||||
/>
|
||||
<span className={styles.CollapsableHeaderTitle}>{pluralizedName}</span>
|
||||
<div className={styles.CollapsableHeaderFiller} />
|
||||
{isOpen ? null : (
|
||||
<div className={styles.TimeBarContainer}>
|
||||
<div
|
||||
className={
|
||||
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
|
||||
}
|
||||
style={{
|
||||
left: left.toFixed(2) + '%',
|
||||
width: width.toFixed(2) + '%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
{isOpen &&
|
||||
suspendedBy.map(({value, index}) => (
|
||||
<SuspendedByRow
|
||||
key={index}
|
||||
index={index}
|
||||
asyncInfo={value}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
skipName={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InspectedElementSuspendedBy({
|
||||
bridge,
|
||||
element,
|
||||
@@ -364,9 +494,31 @@ export default function InspectedElementSuspendedBy({
|
||||
minTime = maxTime - 25;
|
||||
}
|
||||
|
||||
const sortedSuspendedBy = suspendedBy === null ? [] : suspendedBy.slice(0);
|
||||
const sortedSuspendedBy =
|
||||
suspendedBy === null ? [] : suspendedBy.map(withIndex);
|
||||
sortedSuspendedBy.sort(compareTime);
|
||||
|
||||
// Organize into groups of consecutive entries with the same name.
|
||||
const groups = [];
|
||||
let currentGroup = null;
|
||||
let currentGroupName = null;
|
||||
for (let i = 0; i < sortedSuspendedBy.length; i++) {
|
||||
const entry = sortedSuspendedBy[i];
|
||||
const name = entry.value.awaited.name;
|
||||
if (
|
||||
currentGroupName !== name ||
|
||||
!name ||
|
||||
name === 'Promise' ||
|
||||
currentGroup === null
|
||||
) {
|
||||
// Create a new group.
|
||||
currentGroupName = name;
|
||||
currentGroup = [];
|
||||
groups.push(currentGroup);
|
||||
}
|
||||
currentGroup.push(entry);
|
||||
}
|
||||
|
||||
let unknownSuspenders = null;
|
||||
switch (inspectedElement.unknownSuspenders) {
|
||||
case UNKNOWN_SUSPENDERS_REASON_PRODUCTION:
|
||||
@@ -407,19 +559,48 @@ export default function InspectedElementSuspendedBy({
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
</div>
|
||||
{sortedSuspendedBy.map((asyncInfo, index) => (
|
||||
<SuspendedByRow
|
||||
key={index}
|
||||
index={index}
|
||||
asyncInfo={asyncInfo}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
))}
|
||||
{groups.length === 1
|
||||
? // If it's only one type of suspender we can flatten it.
|
||||
groups[0].map(entry => (
|
||||
<SuspendedByRow
|
||||
key={entry.index}
|
||||
index={entry.index}
|
||||
asyncInfo={entry.value}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
))
|
||||
: groups.map((entries, index) =>
|
||||
entries.length === 1 ? (
|
||||
<SuspendedByRow
|
||||
key={entries[0].index}
|
||||
index={entries[0].index}
|
||||
asyncInfo={entries[0].value}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
) : (
|
||||
<SuspendedByGroup
|
||||
key={entries[0].index}
|
||||
name={entries[0].value.awaited.name}
|
||||
suspendedBy={entries}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{unknownSuspenders}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.CallSite, .IgnoredCallSite {
|
||||
.CallSite {
|
||||
display: block;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.IgnoredCallSite {
|
||||
opacity: 0.5;
|
||||
.IgnoredCallSite, .BuiltInCallSite {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.CallSite + .BuiltInCallSite {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Link {
|
||||
|
||||
@@ -60,16 +60,22 @@ export function CallSiteView({
|
||||
symbolicatedCallSite !== null ? symbolicatedCallSite.location : callSite;
|
||||
const ignored =
|
||||
symbolicatedCallSite !== null ? symbolicatedCallSite.ignored : false;
|
||||
if (ignored) {
|
||||
// TODO: Make an option to be able to toggle the display of ignore listed rows.
|
||||
// Ideally this UI should be higher than a single Stack Trace so that there's not
|
||||
// multiple buttons in a single inspection taking up space.
|
||||
return null;
|
||||
}
|
||||
// TODO: Make an option to be able to toggle the display of ignore listed rows.
|
||||
// Ideally this UI should be higher than a single Stack Trace so that there's not
|
||||
// multiple buttons in a single inspection taking up space.
|
||||
|
||||
const isBuiltIn = url === '' || url.startsWith('<anonymous>'); // This looks like a fake anonymous through eval.
|
||||
return (
|
||||
<div className={ignored ? styles.IgnoredCallSite : styles.CallSite}>
|
||||
<div
|
||||
className={
|
||||
ignored
|
||||
? styles.IgnoredCallSite
|
||||
: isBuiltIn
|
||||
? styles.BuiltInCallSite
|
||||
: styles.CallSite
|
||||
}>
|
||||
{functionName || virtualFunctionName}
|
||||
{url !== '' && (
|
||||
{!isBuiltIn && (
|
||||
<>
|
||||
{' @ '}
|
||||
<span
|
||||
|
||||
@@ -361,15 +361,17 @@ export default function DevTools({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={styles.TabContent}
|
||||
hidden={tab !== 'suspense'}>
|
||||
<SuspenseTab
|
||||
portalContainer={
|
||||
suspensePortalContainer
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{enableSuspenseTab && (
|
||||
<div
|
||||
className={styles.TabContent}
|
||||
hidden={tab !== 'suspense'}>
|
||||
<SuspenseTab
|
||||
portalContainer={
|
||||
suspensePortalContainer
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{editorPortalContainer ? (
|
||||
<EditorPane
|
||||
|
||||
@@ -378,7 +378,8 @@ function updateTree(
|
||||
const fiberID = operations[i + 1];
|
||||
const parentID = operations[i + 2];
|
||||
const nameStringID = operations[i + 3];
|
||||
const numRects = operations[i + 4];
|
||||
const isSuspended = operations[i + 4];
|
||||
const numRects = operations[i + 5];
|
||||
const name = stringTable[nameStringID];
|
||||
|
||||
if (__DEBUG__) {
|
||||
@@ -388,16 +389,16 @@ function updateTree(
|
||||
} else {
|
||||
rects =
|
||||
'[' +
|
||||
operations.slice(i + 5, i + 5 + numRects * 4).join(',') +
|
||||
operations.slice(i + 6, i + 6 + numRects * 4).join(',') +
|
||||
']';
|
||||
}
|
||||
debug(
|
||||
'Add suspense',
|
||||
`node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`,
|
||||
`node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID} suspended ${isSuspended}`,
|
||||
);
|
||||
}
|
||||
|
||||
i += 5 + (numRects === -1 ? 0 : numRects * 4);
|
||||
i += 6 + (numRects === -1 ? 0 : numRects * 4);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -459,12 +460,13 @@ function updateTree(
|
||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||
const suspenseNodeId = operations[i++];
|
||||
const hasUniqueSuspenders = operations[i++] === 1;
|
||||
const isSuspended = operations[i++] === 1;
|
||||
const environmentNamesLength = operations[i++];
|
||||
i += environmentNamesLength;
|
||||
if (__DEBUG__) {
|
||||
debug(
|
||||
'Suspender changes',
|
||||
`Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} with ${String(environmentNamesLength)} environments`,
|
||||
`Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,9 @@ export default function SearchInput({
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const {key, metaKey} = event;
|
||||
if (key === 'f' && metaKey) {
|
||||
if (inputRef.current !== null) {
|
||||
inputRef.current.focus();
|
||||
const inputElement = inputRef.current;
|
||||
if (inputElement !== null) {
|
||||
inputElement.focus();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
@@ -75,10 +76,14 @@ export default function SearchInput({
|
||||
// It's important to listen to the ownerDocument to support the browser extension.
|
||||
// Here we use portals to render individual tabs (e.g. Profiler),
|
||||
// and the root document might belong to a different window.
|
||||
const ownerDocument = inputRef.current.ownerDocument;
|
||||
ownerDocument.addEventListener('keydown', handleKeyDown);
|
||||
const ownerDocumentElement = inputRef.current.ownerDocument.documentElement;
|
||||
if (ownerDocumentElement === null) {
|
||||
return;
|
||||
}
|
||||
ownerDocumentElement.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => ownerDocument.removeEventListener('keydown', handleKeyDown);
|
||||
return () =>
|
||||
ownerDocumentElement.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user