Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8335312a16 |
11
.github/workflows/compiler_prereleases.yml
vendored
11
.github/workflows/compiler_prereleases.yml
vendored
@@ -19,9 +19,6 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
@@ -58,13 +55,7 @@ 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'
|
||||
- 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
|
||||
- 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,9 +17,6 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -36,6 +33,5 @@ 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,6 +19,5 @@ 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,6 +82,7 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
@@ -90,10 +91,11 @@ 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 \
|
||||
|
||||
@@ -9,7 +9,7 @@ Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2)
|
||||
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
|
||||
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
|
||||
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
|
||||
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools
|
||||
- [React Performance tracks](https://react.dev/reference/developer-tooling/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools
|
||||
|
||||
### New React DOM Features
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '7.0.0',
|
||||
'eslint-plugin-react-hooks': '6.2.0',
|
||||
'jest-react': '0.18.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
|
||||
@@ -314,36 +314,6 @@ 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,6 +22,7 @@ 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">
|
||||
@@ -35,6 +36,7 @@ export default function AccordionWindow(props: {
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
isFailure={props.isFailure}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -49,6 +51,7 @@ function AccordionWindowItem({
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
isFailure,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
@@ -58,7 +61,7 @@ function AccordionWindowItem({
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
const id = useId();
|
||||
const isShow = tabsOpen.has(name);
|
||||
const isShow = isFailure ? name === 'Output' : tabsOpen.has(name);
|
||||
|
||||
const transitionName = `accordion-window-item-${id}`;
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ import {
|
||||
useState,
|
||||
Suspense,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
@@ -37,7 +35,6 @@ import {BabelFileResult} from '@babel/core';
|
||||
import {
|
||||
CONFIG_PANEL_TRANSITION,
|
||||
TOGGLE_INTERNALS_TRANSITION,
|
||||
EXPAND_ACCORDION_TRANSITION,
|
||||
} from '../../lib/transitionTypes';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
@@ -268,22 +265,8 @@ 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) {
|
||||
@@ -312,6 +295,8 @@ 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>
|
||||
);
|
||||
@@ -330,6 +315,7 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
isFailure={isFailure}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
|
||||
@@ -17,11 +17,15 @@ 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}`;
|
||||
|
||||
@@ -37,7 +41,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 = activeTab === tab;
|
||||
const isActive = currentActiveTab === tab;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
@@ -73,7 +77,7 @@ export default function TabbedWindow({
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(activeTab)}
|
||||
{tabs.get(currentActiveTab)}
|
||||
</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() || !transformOutput) {
|
||||
if (error.hasErrors()) {
|
||||
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.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"zod": "^3.22.4 || ^4.0.0",
|
||||
"zod-validation-error": "^3.0.3 || ^4.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"./**/@babel/parser": "7.7.4",
|
||||
|
||||
@@ -536,8 +536,7 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.StaticComponents:
|
||||
case ErrorCategory.Suppression:
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo:
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
case ErrorCategory.UseMemo: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
@@ -583,10 +582,6 @@ 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
|
||||
*/
|
||||
@@ -674,21 +669,6 @@ 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;
|
||||
@@ -709,14 +689,15 @@ export type LintRule = {
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
|
||||
* any preset.
|
||||
* 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.
|
||||
*
|
||||
* 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!
|
||||
*/
|
||||
preset: LintRulePreset;
|
||||
recommended: boolean;
|
||||
};
|
||||
|
||||
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
|
||||
@@ -739,7 +720,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
@@ -749,7 +730,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Config: {
|
||||
@@ -758,7 +739,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
@@ -767,7 +748,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
@@ -777,7 +758,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectSetState: {
|
||||
@@ -787,7 +768,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',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.ErrorBoundaries: {
|
||||
@@ -797,7 +778,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'error-boundaries',
|
||||
description:
|
||||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Factories: {
|
||||
@@ -808,7 +789,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',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
@@ -817,7 +798,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
@@ -826,7 +807,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
@@ -836,7 +817,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Globals: {
|
||||
@@ -847,7 +828,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)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Hooks: {
|
||||
@@ -861,7 +842,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
|
||||
* this rule.
|
||||
*/
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Immutability: {
|
||||
@@ -871,7 +852,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)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Invariant: {
|
||||
@@ -880,7 +861,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.PreserveManualMemo: {
|
||||
@@ -892,7 +873,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)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Purity: {
|
||||
@@ -902,7 +883,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',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Refs: {
|
||||
@@ -912,7 +893,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)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.RenderSetState: {
|
||||
@@ -922,7 +903,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',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.StaticComponents: {
|
||||
@@ -932,7 +913,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',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Suppression: {
|
||||
@@ -941,7 +922,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Syntax: {
|
||||
@@ -950,7 +931,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Todo: {
|
||||
@@ -959,7 +940,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Hint,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
@@ -969,7 +950,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'unsupported-syntax',
|
||||
description:
|
||||
'Validates against syntax that we do not plan to support in React Compiler',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UseMemo: {
|
||||
@@ -979,17 +960,7 @@ 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.',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'void-use-memo',
|
||||
description:
|
||||
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
@@ -999,7 +970,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'incompatible-library',
|
||||
description:
|
||||
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {z} from 'zod/v4';
|
||||
import {z} from 'zod';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
tryParseExternalFunction,
|
||||
} from '../HIR/Environment';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerPipelineValue} from './Pipeline';
|
||||
|
||||
const PanicThresholdOptionsSchema = z.enum([
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
@@ -83,11 +83,21 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
|
||||
export const USE_FIRE_FUNCTION_NAME = 'useFire';
|
||||
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
|
||||
|
||||
export const MacroSchema = z.string();
|
||||
export const MacroMethodSchema = z.union([
|
||||
z.object({type: z.literal('wildcard')}),
|
||||
z.object({type: z.literal('name'), name: z.string()}),
|
||||
]);
|
||||
|
||||
// Would like to change this to drop the string option, but breaks compatibility with existing configs
|
||||
export const MacroSchema = z.union([
|
||||
z.string(),
|
||||
z.tuple([z.string(), z.array(MacroMethodSchema)]),
|
||||
]);
|
||||
|
||||
export type CompilerMode = 'all_features' | 'no_inferred_memo';
|
||||
|
||||
export type Macro = z.infer<typeof MacroSchema>;
|
||||
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
|
||||
|
||||
const HookSchema = z.object({
|
||||
/*
|
||||
@@ -649,7 +659,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Invalid:
|
||||
* useMemo(() => { ... }, [...]);
|
||||
*/
|
||||
validateNoVoidUseMemo: z.boolean().default(true),
|
||||
validateNoVoidUseMemo: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope
|
||||
|
||||
@@ -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/v4';
|
||||
import {z} from 'zod';
|
||||
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/v4';
|
||||
import {z} from 'zod';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {
|
||||
EffectSchema,
|
||||
|
||||
@@ -438,6 +438,40 @@ export function dropManualMemoization(
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bailout on void return useMemos. This is an anti-pattern where code might be using
|
||||
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
|
||||
* values.
|
||||
*/
|
||||
if (
|
||||
func.env.config.validateNoVoidUseMemo &&
|
||||
manualMemo.kind === 'useMemo'
|
||||
) {
|
||||
const funcToCheck = sidemap.functions.get(
|
||||
fnPlace.identifier.id,
|
||||
)?.value;
|
||||
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
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`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instr.value = getManualMemoizationReplacement(
|
||||
fnPlace,
|
||||
instr.value.loc,
|
||||
@@ -595,3 +629,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,41 +8,13 @@
|
||||
import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
makeInstructionId,
|
||||
MutableRange,
|
||||
Place,
|
||||
ReactiveScope,
|
||||
ReactiveValue,
|
||||
} from '../HIR';
|
||||
import {Macro} from '../HIR/Environment';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Whether a macro requires its arguments to be transitively inlined (eg fbt)
|
||||
* or just avoid having the top-level values be converted to variables (eg fbt.param)
|
||||
*/
|
||||
enum InlineLevel {
|
||||
Transitive = 'Transitive',
|
||||
Shallow = 'Shallow',
|
||||
}
|
||||
type MacroDefinition = {
|
||||
level: InlineLevel;
|
||||
properties: Map<string, MacroDefinition> | null;
|
||||
};
|
||||
|
||||
const SHALLOW_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Shallow,
|
||||
properties: null,
|
||||
};
|
||||
const TRANSITIVE_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Transitive,
|
||||
properties: null,
|
||||
};
|
||||
const FBT_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Transitive,
|
||||
properties: new Map([['*', SHALLOW_MACRO]]),
|
||||
};
|
||||
FBT_MACRO.properties!.set('enum', FBT_MACRO);
|
||||
import {Macro, MacroMethod} from '../HIR/Environment';
|
||||
import {eachReactiveValueOperand} from './visitors';
|
||||
|
||||
/**
|
||||
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
|
||||
@@ -67,210 +39,230 @@ FBT_MACRO.properties!.set('enum', FBT_MACRO);
|
||||
* ## User-defined macro-like function
|
||||
*
|
||||
* Users can also specify their own functions to be treated similarly to fbt via the
|
||||
* `customMacros` environment configuration. By default, user-supplied custom macros
|
||||
* have their arguments transitively inlined.
|
||||
* `customMacros` environment configuration.
|
||||
*/
|
||||
export function memoizeFbtAndMacroOperandsInSameScope(
|
||||
fn: HIRFunction,
|
||||
): Set<IdentifierId> {
|
||||
const macroKinds = new Map<Macro, MacroDefinition>([
|
||||
...Array.from(FBT_TAGS.entries()),
|
||||
...(fn.env.config.customMacros ?? []).map(
|
||||
name => [name, TRANSITIVE_MACRO] as [Macro, MacroDefinition],
|
||||
),
|
||||
const fbtMacroTags = new Set<Macro>([
|
||||
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
|
||||
...(fn.env.config.customMacros ?? []),
|
||||
]);
|
||||
/**
|
||||
* Forward data-flow analysis to identify all macro tags, including
|
||||
* things like `fbt.foo.bar(...)`
|
||||
*/
|
||||
const macroTags = populateMacroTags(fn, macroKinds);
|
||||
|
||||
/**
|
||||
* Reverse data-flow analysis to merge arguments to macro *invocations*
|
||||
* based on the kind of the macro
|
||||
*/
|
||||
const macroValues = mergeMacroArguments(fn, macroTags, macroKinds);
|
||||
|
||||
return macroValues;
|
||||
const fbtValues: Set<IdentifierId> = new Set();
|
||||
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;
|
||||
}
|
||||
}
|
||||
return fbtValues;
|
||||
}
|
||||
|
||||
const FBT_TAGS: Map<string, MacroDefinition> = new Map([
|
||||
['fbt', FBT_MACRO],
|
||||
['fbt:param', SHALLOW_MACRO],
|
||||
['fbt:enum', FBT_MACRO],
|
||||
['fbt:plural', SHALLOW_MACRO],
|
||||
['fbs', FBT_MACRO],
|
||||
['fbs:param', SHALLOW_MACRO],
|
||||
['fbs:enum', FBT_MACRO],
|
||||
['fbs:plural', SHALLOW_MACRO],
|
||||
export const FBT_TAGS: Set<string> = new Set([
|
||||
'fbt',
|
||||
'fbt:param',
|
||||
'fbs',
|
||||
'fbs:param',
|
||||
]);
|
||||
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||
'fbt:param',
|
||||
'fbs:param',
|
||||
]);
|
||||
|
||||
function populateMacroTags(
|
||||
function visit(
|
||||
fn: HIRFunction,
|
||||
macroKinds: Map<Macro, MacroDefinition>,
|
||||
): Map<IdentifierId, MacroDefinition> {
|
||||
const macroTags = new Map<IdentifierId, MacroDefinition>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'Primitive': {
|
||||
if (typeof value.value === 'string') {
|
||||
const macroDefinition = macroKinds.get(value.value);
|
||||
if (macroDefinition != null) {
|
||||
/*
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
* all `fbt` string literals in case they are used as a jsx tag.
|
||||
*/
|
||||
macroTags.set(lvalue.identifier.id, macroDefinition);
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instruction of block.instructions) {
|
||||
const {lvalue, value} = instruction;
|
||||
if (lvalue === null) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
value.kind === 'Primitive' &&
|
||||
typeof value.value === 'string' &&
|
||||
matchesExactTag(value.value, fbtMacroTags)
|
||||
) {
|
||||
/*
|
||||
* 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);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchesExactTag(value.binding.name, fbtMacroTags)
|
||||
) {
|
||||
// Record references to `fbt` as a global
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchTagRoot(value.binding.name, fbtMacroTags) !== null
|
||||
) {
|
||||
const methods = matchTagRoot(value.binding.name, fbtMacroTags)!;
|
||||
macroMethods.set(lvalue.identifier.id, methods);
|
||||
} else if (
|
||||
value.kind === 'PropertyLoad' &&
|
||||
macroMethods.has(value.object.identifier.id)
|
||||
) {
|
||||
const methods = macroMethods.get(value.object.identifier.id)!;
|
||||
const newMethods = [];
|
||||
for (const method of methods) {
|
||||
if (
|
||||
method.length > 0 &&
|
||||
(method[0].type === 'wildcard' ||
|
||||
(method[0].type === 'name' && method[0].name === value.property))
|
||||
) {
|
||||
if (method.length > 1) {
|
||||
newMethods.push(method.slice(1));
|
||||
} else {
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'LoadGlobal': {
|
||||
let macroDefinition = macroKinds.get(value.binding.name);
|
||||
if (macroDefinition != null) {
|
||||
macroTags.set(lvalue.identifier.id, macroDefinition);
|
||||
}
|
||||
break;
|
||||
if (newMethods.length > 0) {
|
||||
macroMethods.set(lvalue.identifier.id, newMethods);
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
if (typeof value.property === 'string') {
|
||||
const macroDefinition = macroTags.get(value.object.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
const propertyDefinition =
|
||||
macroDefinition.properties != null
|
||||
? (macroDefinition.properties.get(value.property) ??
|
||||
macroDefinition.properties.get('*'))
|
||||
: null;
|
||||
const propertyMacro = propertyDefinition ?? macroDefinition;
|
||||
macroTags.set(lvalue.identifier.id, propertyMacro);
|
||||
}
|
||||
} 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)
|
||||
) {
|
||||
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;
|
||||
}
|
||||
break;
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return macroTags;
|
||||
}
|
||||
|
||||
function mergeMacroArguments(
|
||||
fn: HIRFunction,
|
||||
macroTags: Map<IdentifierId, MacroDefinition>,
|
||||
macroKinds: Map<Macro, MacroDefinition>,
|
||||
): Set<IdentifierId> {
|
||||
const macroValues = new Set<IdentifierId>(macroTags.keys());
|
||||
for (const block of Array.from(fn.body.blocks.values()).reverse()) {
|
||||
for (let i = block.instructions.length - 1; i >= 0; i--) {
|
||||
const instr = block.instructions[i]!;
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'DeclareContext':
|
||||
case 'DeclareLocal':
|
||||
case 'Destructure':
|
||||
case 'LoadContext':
|
||||
case 'LoadLocal':
|
||||
case 'PostfixUpdate':
|
||||
case 'PrefixUpdate':
|
||||
case 'StoreContext':
|
||||
case 'StoreLocal': {
|
||||
// Instructions that never need to be merged
|
||||
break;
|
||||
}
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const macroDefinition =
|
||||
macroTags.get(callee.identifier.id) ??
|
||||
macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxExpression': {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
let macroDefinition;
|
||||
if (value.tag.kind === 'Identifier') {
|
||||
macroDefinition = macroTags.get(value.tag.identifier.id);
|
||||
} else {
|
||||
macroDefinition = macroKinds.get(value.tag.name);
|
||||
}
|
||||
macroDefinition ??= macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const macroDefinition = macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
|
||||
return Array.from(tags).some(macro =>
|
||||
typeof macro === 'string'
|
||||
? s === macro
|
||||
: macro[1].length === 0 && macro[0] === s,
|
||||
);
|
||||
}
|
||||
|
||||
function matchTagRoot(
|
||||
s: string,
|
||||
tags: Set<Macro>,
|
||||
): Array<Array<MacroMethod>> | null {
|
||||
const methods: Array<Array<MacroMethod>> = [];
|
||||
for (const macro of tags) {
|
||||
if (typeof macro === 'string') {
|
||||
continue;
|
||||
}
|
||||
for (const phi of block.phis) {
|
||||
const scope = phi.place.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const macroDefinition = macroTags.get(phi.place.identifier.id);
|
||||
if (
|
||||
macroDefinition == null ||
|
||||
macroDefinition.level === InlineLevel.Shallow
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
macroValues.add(phi.place.identifier.id);
|
||||
for (const operand of phi.operands.values()) {
|
||||
operand.identifier.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
|
||||
macroTags.set(operand.identifier.id, macroDefinition);
|
||||
macroValues.add(operand.identifier.id);
|
||||
}
|
||||
const [tag, rest] = macro;
|
||||
if (tag === s && rest.length > 0) {
|
||||
methods.push(rest);
|
||||
}
|
||||
}
|
||||
return macroValues;
|
||||
if (methods.length > 0) {
|
||||
return methods;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isFbtCallExpression(
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'CallExpression' &&
|
||||
fbtValues.has(value.callee.identifier.id)) ||
|
||||
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxExpression(
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
value.kind === 'JsxExpression' &&
|
||||
((value.tag.kind === 'Identifier' &&
|
||||
fbtValues.has(value.tag.identifier.id)) ||
|
||||
(value.tag.kind === 'BuiltinTag' &&
|
||||
matchesExactTag(value.tag.name, fbtMacroTags)))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxChild(
|
||||
fbtValues: Set<IdentifierId>,
|
||||
lvalue: Place | null,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
|
||||
lvalue !== null &&
|
||||
fbtValues.has(lvalue.identifier.id)
|
||||
);
|
||||
}
|
||||
|
||||
function expandFbtScopeRange(
|
||||
@@ -283,22 +275,3 @@ function expandFbtScopeRange(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function visitOperands(
|
||||
macroDefinition: MacroDefinition,
|
||||
scope: ReactiveScope,
|
||||
lvalue: Place,
|
||||
value: InstructionValue,
|
||||
macroValues: Set<IdentifierId>,
|
||||
macroTags: Map<IdentifierId, MacroDefinition>,
|
||||
): void {
|
||||
macroValues.add(lvalue.identifier.id);
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (macroDefinition.level === InlineLevel.Transitive) {
|
||||
operand.identifier.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
|
||||
macroTags.set(operand.identifier.id, macroDefinition);
|
||||
}
|
||||
macroValues.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ function* generateInstructionTypes(
|
||||
shapeId: BuiltInArrayId,
|
||||
});
|
||||
} else {
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
@@ -135,7 +135,16 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
} else if (val) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'customMacros' && typeof parsedVal === 'string') {
|
||||
maybeConfig[key] = [parsedVal.split('.')[0]];
|
||||
const valSplit = parsedVal.split('.');
|
||||
const props = [];
|
||||
for (const elt of valSplit.slice(1)) {
|
||||
if (elt === '*') {
|
||||
props.push({type: 'wildcard'});
|
||||
} else if (elt.length > 0) {
|
||||
props.push({type: 'name', name: elt});
|
||||
}
|
||||
}
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
continue;
|
||||
}
|
||||
maybeConfig[key] = parsedVal;
|
||||
|
||||
@@ -10,37 +10,16 @@ import {
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
const voidMemoErrors = new 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') {
|
||||
@@ -66,8 +45,10 @@ 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 : value.property;
|
||||
const isUseMemo = useMemos.has(callee.identifier.id);
|
||||
value.kind === 'CallExpression'
|
||||
? value.callee.identifier.id
|
||||
: value.property.identifier.id;
|
||||
const isUseMemo = useMemos.has(callee);
|
||||
if (!isUseMemo || value.args.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -123,103 +104,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
);
|
||||
}
|
||||
|
||||
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
|
||||
|
||||
if (fn.env.config.validateNoVoidUseMemo) {
|
||||
if (!hasNonVoidReturn(body.loweredFunc.func)) {
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: body.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
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()) {
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() result is unused',
|
||||
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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
fn.env.logErrors(voidMemoErrors.asResult());
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
|
||||
## 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 | }, []);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
function Component() {
|
||||
let x;
|
||||
const y = useMemo(() => {
|
||||
let z;
|
||||
x = [];
|
||||
z = true;
|
||||
return z;
|
||||
}, []);
|
||||
return [x, y];
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
## 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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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,64 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
const value2 = React.useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
error.useMemo-no-return-value.ts:3:16
|
||||
1 | // @validateNoVoidUseMemo
|
||||
2 | function Component() {
|
||||
> 3 | const value = useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 4 | console.log('computing');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 5 | }, []);
|
||||
| ^^^^^^^^^ useMemo() callbacks must return a value
|
||||
6 | const value2 = React.useMemo(() => {
|
||||
7 | console.log('computing');
|
||||
8 | }, []);
|
||||
|
||||
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.
|
||||
|
||||
error.useMemo-no-return-value.ts:6:17
|
||||
4 | console.log('computing');
|
||||
5 | }, []);
|
||||
> 6 | const value2 = React.useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | console.log('computing');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 8 | }, []);
|
||||
| ^^^^^^^^^ useMemo() callbacks must return a value
|
||||
9 | return (
|
||||
10 | <div>
|
||||
11 | {value}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
// @validateNoVoidUseMemo
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
@@ -1,5 +1,9 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {identity} from 'shared-runtime';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
@@ -12,25 +16,22 @@ import {identity} from 'shared-runtime';
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
<Stringify>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', identity(firstname)),
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' + fbt.param('lastname', identity(lastname)),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,3 +40,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
---
|
||||
t3
|
||||
---
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fbt from 'fbt';
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
@@ -12,7 +12,7 @@ import {Stringify, identity} from 'shared-runtime';
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
<Stringify>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
@@ -20,18 +20,14 @@ function Component({firstname, lastname}) {
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' +
|
||||
fbt.param('lastname', <Stringify key={1} name={lastname} />),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,23 +44,15 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(5);
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] !== props.count || $[1] !== props.option) {
|
||||
let t1;
|
||||
if ($[3] !== props.count) {
|
||||
t1 = identity(props.count);
|
||||
$[3] = props.count;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = (
|
||||
<span>
|
||||
{fbt._(
|
||||
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
|
||||
[
|
||||
fbt._plural(t1, "count"),
|
||||
fbt._plural(identity(props.count), "count"),
|
||||
fbt._param(
|
||||
"option",
|
||||
|
||||
|
||||
@@ -44,23 +44,15 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(5);
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] !== props.count || $[1] !== props.option) {
|
||||
let t1;
|
||||
if ($[3] !== props.count) {
|
||||
t1 = identity(props.count);
|
||||
$[3] = props.count;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = (
|
||||
<span>
|
||||
{fbt._(
|
||||
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
|
||||
[
|
||||
fbt._plural(t1, "count"),
|
||||
fbt._plural(identity(props.count), "count"),
|
||||
fbt._param(
|
||||
"option",
|
||||
|
||||
|
||||
@@ -37,47 +37,28 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Foo(t0) {
|
||||
const $ = _c(13);
|
||||
const $ = _c(3);
|
||||
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] !== name1 || $[6] !== t2) {
|
||||
t3 = <span key={name1}>{t2}</span>;
|
||||
$[5] = name1;
|
||||
$[6] = t2;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== name2) {
|
||||
t4 = <b>{name2}</b>;
|
||||
$[8] = name2;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== name2 || $[11] !== t4) {
|
||||
t5 = <span key={name2}>{t4}</span>;
|
||||
$[10] = name2;
|
||||
$[11] = t4;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
t1 = fbt._(
|
||||
"{user1} and {user2} accepted your PR!",
|
||||
[fbt._param("user1", t3), fbt._param("user2", t5)],
|
||||
[
|
||||
fbt._param(
|
||||
"user1",
|
||||
|
||||
<span key={name1}>
|
||||
<b>{name1}</b>
|
||||
</span>,
|
||||
),
|
||||
fbt._param(
|
||||
"user2",
|
||||
|
||||
<span key={name2}>
|
||||
<b>{name2}</b>
|
||||
</span>,
|
||||
),
|
||||
],
|
||||
{ hk: "2PxMie" },
|
||||
);
|
||||
$[0] = name1;
|
||||
|
||||
@@ -29,24 +29,20 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const $ = _c(4);
|
||||
const { name, data, icon } = t0;
|
||||
let t1;
|
||||
if ($[0] !== data || $[1] !== icon || $[2] !== name) {
|
||||
let t2;
|
||||
if ($[4] !== name) {
|
||||
t2 = <Text type="h4">{name}</Text>;
|
||||
$[4] = name;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
t1 = (
|
||||
<Text type="body4">
|
||||
{fbt._(
|
||||
"{item author}{icon}{=m2}",
|
||||
[
|
||||
fbt._param("item author", t2),
|
||||
fbt._param(
|
||||
"item author",
|
||||
|
||||
<Text type="h4">{name}</Text>,
|
||||
),
|
||||
fbt._param(
|
||||
"icon",
|
||||
|
||||
|
||||
@@ -27,21 +27,16 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.text) {
|
||||
const t1 = identity(props.text);
|
||||
let t2;
|
||||
if ($[2] !== t1) {
|
||||
t2 = <>{t1}</>;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
t0 = (
|
||||
<Foo
|
||||
value={fbt._("{value}%", [fbt._param("value", t2)], { hk: "10F5Cc" })}
|
||||
value={fbt._(
|
||||
"{value}%",
|
||||
[fbt._param("value", <>{identity(props.text)}</>)],
|
||||
{ hk: "10F5Cc" },
|
||||
)}
|
||||
/>
|
||||
);
|
||||
$[0] = props.text;
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Example({x}) {
|
||||
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
|
||||
// be memoized separately
|
||||
return (
|
||||
<fbt desc="Description">
|
||||
Outer Text
|
||||
<Foo key="b" x={x}>
|
||||
<Bar key="a">Inner Text</Bar>
|
||||
</Foo>
|
||||
</fbt>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo({x, children}) {
|
||||
'use no memo';
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
'use no memo';
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{x: 'Hello'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { fbt } from "fbt";
|
||||
|
||||
function Example(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = fbt._(
|
||||
"Outer Text {=m1}",
|
||||
[
|
||||
fbt._implicitParam(
|
||||
"=m1",
|
||||
|
||||
<Foo key="b" x={x}>
|
||||
{fbt._(
|
||||
"{=m1}",
|
||||
[
|
||||
fbt._implicitParam(
|
||||
"=m1",
|
||||
<Bar key="a">
|
||||
{fbt._("Inner Text", null, { hk: "32YB0l" })}
|
||||
</Bar>,
|
||||
),
|
||||
],
|
||||
{ hk: "23dJsI" },
|
||||
)}
|
||||
</Foo>,
|
||||
),
|
||||
],
|
||||
{ hk: "2RVA7V" },
|
||||
);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
function Foo({ x, children }) {
|
||||
"use no memo";
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({ children }) {
|
||||
"use no memo";
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{ x: "Hello" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) Outer Text <div>Hello</div><span>Inner Text</span>
|
||||
@@ -1,35 +0,0 @@
|
||||
// @flow
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Example({x}) {
|
||||
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
|
||||
// be memoized separately
|
||||
return (
|
||||
<fbt desc="Description">
|
||||
Outer Text
|
||||
<Foo key="b" x={x}>
|
||||
<Bar key="a">Inner Text</Bar>
|
||||
</Foo>
|
||||
</fbt>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo({x, children}) {
|
||||
'use no memo';
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
'use no memo';
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{x: 'Hello'}],
|
||||
};
|
||||
@@ -1,128 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {Stringify, identity} 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 (
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' +
|
||||
fbt.param('lastname', <Stringify key={1} name={lastname} />),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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, identity } 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(9);
|
||||
const { firstname, lastname } = t0;
|
||||
let t1;
|
||||
if ($[0] !== firstname || $[1] !== lastname) {
|
||||
let t2;
|
||||
if ($[3] !== firstname) {
|
||||
t2 = <Stringify key={0} name={firstname} />;
|
||||
$[3] = firstname;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== lastname) {
|
||||
t3 = <Stringify key={1} name={lastname} />;
|
||||
$[5] = lastname;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
t1 = fbt._(
|
||||
"Name: {firstname}, {lastname}",
|
||||
[
|
||||
fbt._param("firstname", t2),
|
||||
fbt._param(
|
||||
"lastname",
|
||||
identity(
|
||||
fbt._("(inner){lastname}", [fbt._param("lastname", t3)], {
|
||||
hk: "1Kdxyo",
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
{ hk: "3AiIf8" },
|
||||
);
|
||||
$[0] = firstname;
|
||||
$[1] = lastname;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== t1) {
|
||||
t2 = <div>{t1}</div>;
|
||||
$[7] = t1;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstname: "first", lastname: "last" }],
|
||||
sequentialRenders: [{ firstname: "first", lastname: "last" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Name: <div>{"name":"first"}</div>, (inner)<div>{"name":"last"}</div></div>
|
||||
@@ -1,124 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {identity} 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 (
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', identity(firstname)),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' + fbt.param('lastname', identity(lastname)),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 { identity } 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",
|
||||
|
||||
identity(firstname),
|
||||
),
|
||||
fbt._param(
|
||||
"lastname",
|
||||
|
||||
identity(
|
||||
fbt._(
|
||||
"(inner){lastname}",
|
||||
[
|
||||
fbt._param(
|
||||
"lastname",
|
||||
|
||||
identity(lastname),
|
||||
),
|
||||
],
|
||||
{ hk: "1Kdxyo" },
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
{ hk: "3AiIf8" },
|
||||
);
|
||||
$[0] = firstname;
|
||||
$[1] = lastname;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = <div>{t1}</div>;
|
||||
$[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>Name: first, (inner)last</div>
|
||||
@@ -1,78 +0,0 @@
|
||||
|
||||
## 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.
|
||||
@@ -1,22 +0,0 @@
|
||||
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: [{}],
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
## 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",
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
function t(props) {
|
||||
let [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
@@ -37,7 +37,7 @@ function Component(props) {
|
||||
const $ = _c(16);
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = idx(props, (_) => _.group.label);
|
||||
t0 = idx(props, _temp);
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
@@ -46,7 +46,7 @@ function Component(props) {
|
||||
const groupName1 = t0;
|
||||
let t1;
|
||||
if ($[2] !== props) {
|
||||
t1 = idx.a(props, (__0) => __0.group.label);
|
||||
t1 = idx.a(props, _temp2);
|
||||
$[2] = props;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
@@ -73,7 +73,7 @@ function Component(props) {
|
||||
const groupName4 = t3;
|
||||
let t4;
|
||||
if ($[8] !== props) {
|
||||
t4 = idx.hello_world.b.c(props, (__3) => __3.group.label);
|
||||
t4 = idx.hello_world.b.c(props, _temp3);
|
||||
$[8] = props;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
@@ -108,6 +108,15 @@ function Component(props) {
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
function _temp3(__3) {
|
||||
return __3.group.label;
|
||||
}
|
||||
function _temp2(__0) {
|
||||
return __0.group.label;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.group.label;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ function Component(props) {
|
||||
const $ = _c(10);
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = idx(props, (_) => _.group.label);
|
||||
t0 = idx(props, _temp);
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
@@ -49,7 +49,7 @@ function Component(props) {
|
||||
const groupName2 = t1;
|
||||
let t2;
|
||||
if ($[4] !== props) {
|
||||
t2 = idx.a.b(props, (__1) => __1.group.label);
|
||||
t2 = idx.a.b(props, _temp2);
|
||||
$[4] = props;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
@@ -74,6 +74,12 @@ function Component(props) {
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
function _temp2(__1) {
|
||||
return __1.group.label;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.group.label;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() result is unused","description":"This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":2,"index":67},"end":{"line":3,"column":9,"index":74},"filename":"invalid-unused-usememo.ts","identifierName":"useMemo"},"message":"useMemo() result is unused"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":7,"column":1,"index":127},"filename":"invalid-unused-usememo.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,7 +0,0 @@
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
const value2 = React.useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
|
||||
console.log("computing");
|
||||
|
||||
console.log("computing");
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (
|
||||
<div>
|
||||
{undefined}
|
||||
{undefined}
|
||||
</div>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":24,"index":89},"end":{"line":5,"column":3,"index":130},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":6,"column":31,"index":168},"end":{"line":8,"column":3,"index":209},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":15,"column":1,"index":283},"filename":"invalid-useMemo-no-return-value.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
}, []);
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
mutate(a);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":18,"index":61},"end":{"line":5,"column":3,"index":87},"filename":"invalid-useMemo-return-empty.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":19},"end":{"line":7,"column":1,"index":107},"filename":"invalid-useMemo-return-empty.ts"},"fnName":"component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":1,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
## 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 ]]"
|
||||
@@ -1,15 +0,0 @@
|
||||
// @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: [],
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
|
||||
## 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
|
||||
@@ -1,15 +0,0 @@
|
||||
// @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: [],
|
||||
};
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const item = props.item;
|
||||
const thumbnails = [];
|
||||
@@ -23,7 +22,7 @@ function Component(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo:false
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const item = props.item;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const item = props.item;
|
||||
const thumbnails = [];
|
||||
|
||||
@@ -6,7 +6,6 @@ function Component(props) {
|
||||
const x = useMemo(() => {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
return props.value;
|
||||
}
|
||||
}
|
||||
}, [props.cond]);
|
||||
@@ -25,18 +24,10 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
let t0;
|
||||
bb0: {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
t0 = props.value;
|
||||
break bb0;
|
||||
}
|
||||
}
|
||||
t0 = undefined;
|
||||
}
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -2,7 +2,6 @@ function Component(props) {
|
||||
const x = useMemo(() => {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
return props.value;
|
||||
}
|
||||
}
|
||||
}, [props.cond]);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
}, []);
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function component(a) {
|
||||
mutate(a);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
@@ -14,7 +14,6 @@ export {
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
LintRules,
|
||||
LintRulePreset,
|
||||
type CompilerErrorDetailOptions,
|
||||
type CompilerDiagnosticOptions,
|
||||
type CompilerDiagnosticDetail,
|
||||
|
||||
@@ -120,15 +120,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
|
||||
|
||||
return <Child x={state} />;
|
||||
}`,
|
||||
errors: [
|
||||
makeTestCaseError('useMemo() callbacks must return a value'),
|
||||
makeTestCaseError(
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
),
|
||||
makeTestCaseError(
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
),
|
||||
],
|
||||
errors: [makeTestCaseError('useMemo() callbacks must return a value')],
|
||||
},
|
||||
{
|
||||
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.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"zod": "^3.22.4 || ^4.0.0",
|
||||
"zod-validation-error": "^3.0.3 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.4",
|
||||
|
||||
@@ -15,7 +15,6 @@ 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';
|
||||
@@ -151,7 +150,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: rule.description,
|
||||
recommended: rule.preset === LintRulePreset.Recommended,
|
||||
recommended: rule.recommended,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
@@ -172,16 +171,7 @@ export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
|
||||
}, {} as RulesConfig);
|
||||
|
||||
export const recommendedRules: RulesConfig = LintRules.filter(
|
||||
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,
|
||||
rule => rule.recommended,
|
||||
).reduce((acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
|
||||
@@ -10,14 +10,7 @@ import {defineConfig} from 'tsup';
|
||||
export default defineConfig({
|
||||
entry: ['./src/index.ts'],
|
||||
outDir: './dist',
|
||||
external: [
|
||||
'@babel/core',
|
||||
'hermes-parser',
|
||||
'zod',
|
||||
'zod/v4',
|
||||
'zod-validation-error',
|
||||
'zod-validation-error/v4',
|
||||
],
|
||||
external: ['@babel/core', 'hermes-parser', 'zod', 'zod-validation-error'],
|
||||
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.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"zod": "^3.22.4 || ^4.0.0",
|
||||
"zod-validation-error": "^3.0.3 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"engines": {
|
||||
|
||||
@@ -18,9 +18,7 @@ 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.25.0 || ^4.0.0"
|
||||
"zod": "^3.22.4 || ^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/v4';
|
||||
import {z} from 'zod';
|
||||
import {compile, type PrintedCompilerPipelineValue} from './compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
|
||||
@@ -37,9 +37,7 @@
|
||||
"react": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"readline": "^1.3.0",
|
||||
"yargs": "^17.7.1",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"yargs": "^17.7.1"
|
||||
},
|
||||
"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/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
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', 'latest'],
|
||||
choices: ['experimental', 'beta', 'rc'],
|
||||
default: 'experimental',
|
||||
})
|
||||
.option('tag-version', {
|
||||
@@ -145,15 +145,10 @@ async function main() {
|
||||
files: {exclude: ['.DS_Store']},
|
||||
});
|
||||
const truncatedHash = hash.slice(0, 7);
|
||||
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}`;
|
||||
}
|
||||
let 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}`;
|
||||
}
|
||||
@@ -186,9 +181,21 @@ async function main() {
|
||||
if (otp != null) {
|
||||
opts.push(`--otp=${otp}`);
|
||||
}
|
||||
|
||||
opts.push(`--tag=${argv.tag}`);
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
try {
|
||||
await spawnHelper(
|
||||
'npm',
|
||||
|
||||
@@ -11505,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@^3.5.0 || ^4.0.0":
|
||||
"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 || ^4.0.0":
|
||||
version "4.1.11"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5"
|
||||
integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==
|
||||
|
||||
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.scrollIntoView(alignToTop);
|
||||
fragmentRef.current.experimental_scrollIntoView(alignToTop);
|
||||
};
|
||||
|
||||
const scrollVerticalNoChildren = () => {
|
||||
noChildRef.current.scrollIntoView(alignToTop);
|
||||
noChildRef.current.experimental_scrollIntoView(alignToTop);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["plugin:react-hooks/recommended"],
|
||||
"extends": ["plugin:react-hooks/recommended-latest-legacy"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["plugin:react-hooks/recommended"],
|
||||
"extends": ["plugin:react-hooks/recommended-latest-legacy"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["plugin:react-hooks/recommended"],
|
||||
"extends": ["plugin:react-hooks/recommended-latest-legacy"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
|
||||
@@ -2,7 +2,6 @@ import {defineConfig} from 'eslint/config';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
||||
export default defineConfig([
|
||||
reactHooks.configs.flat['recommended-latest'],
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
@@ -13,6 +12,10 @@ export default defineConfig([
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
extends: ['react-hooks/recommended-latest'],
|
||||
rules: {
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
},
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.mjs && yarn",
|
||||
"lint": "tsc --noEmit && eslint index.js --report-unused-disable-directives"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.3"
|
||||
"lint": "eslint index.js --report-unused-disable-directives"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,2 @@
|
||||
{
|
||||
"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,6 +48,13 @@
|
||||
"@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"
|
||||
@@ -59,11 +66,32 @@
|
||||
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"
|
||||
@@ -81,6 +109,35 @@
|
||||
"@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"
|
||||
@@ -111,6 +168,14 @@
|
||||
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"
|
||||
@@ -133,7 +198,7 @@
|
||||
"@babel/types" "^7.28.4"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/types@^7.27.1", "@babel/types@^7.28.2", "@babel/types@^7.28.4":
|
||||
"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@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==
|
||||
@@ -603,6 +668,18 @@ 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"
|
||||
@@ -835,11 +912,6 @@ 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"
|
||||
@@ -877,12 +949,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 || ^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.5.3"
|
||||
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.5.3.tgz#85ba33290200d8db9f043621e284f40dddefb7e5"
|
||||
integrity sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==
|
||||
|
||||
"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==
|
||||
zod@^3.22.4:
|
||||
version "3.25.76"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
|
||||
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, {
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useState,
|
||||
addTransitionType,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
} from 'react';
|
||||
|
||||
import Chrome from './Chrome.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {
|
||||
addTransitionType,
|
||||
ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
Activity,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
## 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))
|
||||
|
||||
@@ -4,6 +4,8 @@ The official ESLint plugin for [React](https://react.dev) which enforces the [Ru
|
||||
|
||||
## 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
|
||||
@@ -16,7 +18,9 @@ yarn add eslint-plugin-react-hooks --dev
|
||||
|
||||
### Flat Config (eslint.config.js|ts)
|
||||
|
||||
Add the `recommended` config for all recommended rules:
|
||||
#### >= 6.0.0
|
||||
|
||||
For users of 6.0 and beyond, add the `recommended` config.
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
@@ -24,32 +28,61 @@ import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export default defineConfig([
|
||||
reactHooks.configs.flat.recommended,
|
||||
{
|
||||
files: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
extends: ['react-hooks/recommended'],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
If you want to try bleeding edge experimental compiler rules, use `recommended-latest`.
|
||||
#### 5.2.0
|
||||
|
||||
For users of 5.2.0 (the first version with flat config support), add the `recommended-latest` config.
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export default defineConfig([
|
||||
reactHooks.configs.flat['recommended-latest'],
|
||||
{
|
||||
files: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
extends: ['react-hooks/recommended-latest'],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
### Legacy Config (.eslintrc)
|
||||
|
||||
If you are still using ESLint below 9.0.0, the `recommended` preset can also be used to enable all recommended rules.
|
||||
#### >= 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.
|
||||
|
||||
```js
|
||||
{
|
||||
"extends": ["plugin:react-hooks/recommended"],
|
||||
// ...
|
||||
"extends": [
|
||||
// ...
|
||||
"plugin:react-hooks/recommended-legacy"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### < 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
|
||||
@@ -59,7 +92,7 @@ If you want more fine-grained configuration, you can instead choose to enable sp
|
||||
#### Flat Config (eslint.config.js|ts)
|
||||
|
||||
```js
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import * as reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -67,26 +100,8 @@ 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',
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -101,26 +116,8 @@ export default [
|
||||
],
|
||||
"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"
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1555,17 +1555,6 @@ const allTests = {
|
||||
`,
|
||||
errors: [useEffectEventError('onClick', false)],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Invalid because useEffectEvent is being passed down
|
||||
function MyComponent({ theme }) {
|
||||
return <Child onClick={useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
})} />;
|
||||
}
|
||||
`,
|
||||
errors: [{...useEffectEventError(null, false), line: 4}],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// This should error even though it shares an identifier name with the below
|
||||
@@ -1737,14 +1726,6 @@ function classError(hook) {
|
||||
}
|
||||
|
||||
function useEffectEventError(fn, called) {
|
||||
if (fn === null) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "useEffectEvent" can only be called at the top level of your component.` +
|
||||
` It cannot be passed down.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message:
|
||||
`\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export {default} from './cjs/eslint-plugin-react-hooks';
|
||||
export * from './cjs/eslint-plugin-react-hooks';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-plugin-react-hooks",
|
||||
"description": "ESLint rules for React Hooks",
|
||||
"version": "7.0.0",
|
||||
"version": "5.2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
@@ -41,16 +41,15 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"hermes-parser": "^0.25.1",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"zod": "^3.22.4 || ^4.0.0",
|
||||
"zod-validation-error": "^3.0.3 || ^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/eslint": "^8.56.12",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/estree-jsx": "^1.0.5",
|
||||
"@types/node": "^20.2.5",
|
||||
|
||||
@@ -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,12 +23,13 @@ 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 recommendedCompilerRuleConfigs = Object.fromEntries(
|
||||
const compilerRuleConfigs = Object.fromEntries(
|
||||
Object.entries(recommendedRules).map(([name, ruleConfig]) => {
|
||||
return [
|
||||
`react-hooks/${name}` as const,
|
||||
@@ -37,61 +38,69 @@ const recommendedCompilerRuleConfigs = Object.fromEntries(
|
||||
}),
|
||||
) as Record<`react-hooks/${string}`, Linter.RuleEntry>;
|
||||
|
||||
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 = {
|
||||
// All rules including compiler rules (for recommended-latest config)
|
||||
const allRuleConfigs: Linter.RulesRecord = {
|
||||
...basicRuleConfigs,
|
||||
...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>,
|
||||
...compilerRuleConfigs,
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
meta: {
|
||||
name: 'eslint-plugin-react-hooks',
|
||||
version: '7.0.0',
|
||||
},
|
||||
rules,
|
||||
configs,
|
||||
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>;
|
||||
},
|
||||
};
|
||||
|
||||
Object.assign(configs.flat, {
|
||||
'recommended-latest': {
|
||||
plugins: {'react-hooks': plugin},
|
||||
rules: configs['recommended-latest'].rules,
|
||||
Object.assign(plugin.configs, {
|
||||
'recommended-legacy': {
|
||||
plugins: ['react-hooks'],
|
||||
rules: basicRuleConfigs,
|
||||
},
|
||||
recommended: {
|
||||
plugins: {'react-hooks': plugin},
|
||||
rules: configs.recommended.rules,
|
||||
|
||||
'recommended-latest-legacy': {
|
||||
plugins: ['react-hooks'],
|
||||
rules: allRuleConfigs,
|
||||
},
|
||||
|
||||
'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;
|
||||
|
||||
@@ -171,15 +171,7 @@ function isUseEffectEventIdentifier(node: Node): boolean {
|
||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||
}
|
||||
|
||||
function useEffectEventError(fn: string | null, called: boolean): string {
|
||||
// no function identifier, i.e. it is not assigned to a variable
|
||||
if (fn === null) {
|
||||
return (
|
||||
`React Hook "useEffectEvent" can only be called at the top level of your component.` +
|
||||
` It cannot be passed down.`
|
||||
);
|
||||
}
|
||||
|
||||
function useEffectEventError(fn: string, called: boolean): string {
|
||||
return (
|
||||
`\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
'Effects and Effect Events in the same component.' +
|
||||
@@ -780,22 +772,6 @@ const rule = {
|
||||
// comparison later when we exit
|
||||
lastEffect = node;
|
||||
}
|
||||
|
||||
// Specifically disallow <Child onClick={useEffectEvent(...)} /> because this
|
||||
// case can't be caught by `recordAllUseEffectEventFunctions` as it isn't assigned to a variable
|
||||
if (
|
||||
isUseEffectEventIdentifier(nodeWithoutNamespace) &&
|
||||
node.parent?.type !== 'VariableDeclarator' &&
|
||||
// like in other hooks, calling useEffectEvent at component's top level without assignment is valid
|
||||
node.parent?.type !== 'ExpressionStatement'
|
||||
) {
|
||||
const message = useEffectEventError(null, false);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Identifier(node) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
LintRules,
|
||||
type LintRule,
|
||||
ErrorSeverity,
|
||||
LintRulePreset,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {type Linter, type Rule} from 'eslint';
|
||||
import runReactCompiler, {RunCacheEntry} from './RunReactCompiler';
|
||||
@@ -150,7 +149,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: rule.description,
|
||||
recommended: rule.preset === LintRulePreset.Recommended,
|
||||
recommended: rule.recommended,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
@@ -165,26 +164,23 @@ 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.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);
|
||||
rule => rule.recommended,
|
||||
).reduce(
|
||||
(acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
},
|
||||
{} as RulesConfig,
|
||||
);
|
||||
|
||||
export function mapErrorSeverityToESlint(
|
||||
severity: ErrorSeverity,
|
||||
|
||||
@@ -17,7 +17,6 @@ import BabelPluginReactCompiler, {
|
||||
LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import type {SourceCode} from 'eslint';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import {isDeepStrictEqual} from 'util';
|
||||
import type {ParseResult} from '@babel/parser';
|
||||
|
||||
@@ -114,28 +113,14 @@ function runReactCompilerImpl({
|
||||
}
|
||||
|
||||
let babelAST: ParseResult<File> | null = null;
|
||||
|
||||
if (filename.endsWith('.tsx') || filename.endsWith('.ts')) {
|
||||
try {
|
||||
babelAST = babelParse(sourceCode.text, {
|
||||
sourceFilename: filename,
|
||||
sourceType: 'unambiguous',
|
||||
plugins: ['typescript', 'jsx'],
|
||||
});
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
babelAST = HermesParser.parse(sourceCode.text, {
|
||||
babel: true,
|
||||
enableExperimentalComponentSyntax: true,
|
||||
sourceFilename: filename,
|
||||
sourceType: 'module',
|
||||
});
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
try {
|
||||
babelAST = babelParse(sourceCode.text, {
|
||||
sourceFilename: filename,
|
||||
sourceType: 'unambiguous',
|
||||
plugins: ['typescript', 'jsx'],
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
if (babelAST != null) {
|
||||
|
||||
99
packages/react-client/src/ReactFlightClient.js
vendored
99
packages/react-client/src/ReactFlightClient.js
vendored
@@ -367,7 +367,6 @@ 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
|
||||
@@ -501,7 +500,7 @@ function createErrorChunk<T>(
|
||||
}
|
||||
|
||||
function moveDebugInfoFromChunkToInnerValue<T>(
|
||||
chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
|
||||
chunk: InitializedChunk<T>,
|
||||
value: T,
|
||||
): void {
|
||||
// Remove the debug info from the initialized chunk, and add it to the inner
|
||||
@@ -1570,10 +1569,6 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1818,6 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2545,10 +2536,6 @@ function missingCall() {
|
||||
);
|
||||
}
|
||||
|
||||
function markIOStarted(this: Response) {
|
||||
this._debugIOStarted = true;
|
||||
}
|
||||
|
||||
function ResponseInstance(
|
||||
this: $FlowFixMe,
|
||||
bundlerConfig: ServerConsumerModuleMap,
|
||||
@@ -2622,10 +2609,6 @@ 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;
|
||||
@@ -2779,7 +2762,7 @@ function incrementChunkDebugInfo(
|
||||
}
|
||||
}
|
||||
|
||||
function addAsyncInfo(chunk: SomeChunk<any>, asyncInfo: ReactAsyncInfo): void {
|
||||
function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
|
||||
const value = resolveLazy(chunk.value);
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
@@ -2791,39 +2774,34 @@ function addAsyncInfo(chunk: SomeChunk<any>, asyncInfo: ReactAsyncInfo): void {
|
||||
) {
|
||||
if (isArray(value._debugInfo)) {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
value._debugInfo.push(asyncInfo);
|
||||
value._debugInfo.push.apply(value._debugInfo, debugInfo);
|
||||
} else {
|
||||
Object.defineProperty((value: any), '_debugInfo', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: [asyncInfo],
|
||||
value: debugInfo,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
chunk._debugInfo.push(asyncInfo);
|
||||
chunk._debugInfo.push.apply(chunk._debugInfo, debugInfo);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChunkDebugInfo(
|
||||
response: Response,
|
||||
streamState: StreamState,
|
||||
chunk: SomeChunk<any>,
|
||||
): void {
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2859,12 +2837,12 @@ function resolveModel(
|
||||
model,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
resolveModelChunk(response, chunk, model);
|
||||
}
|
||||
@@ -2891,7 +2869,7 @@ function resolveText(
|
||||
}
|
||||
const newChunk = createInitializedTextChunk(response, text);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
}
|
||||
@@ -2917,7 +2895,7 @@ function resolveBuffer(
|
||||
}
|
||||
const newChunk = createInitializedBufferChunk(response, buffer);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
}
|
||||
@@ -2964,7 +2942,7 @@ function resolveModule(
|
||||
blockedChunk.status = BLOCKED;
|
||||
}
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, blockedChunk);
|
||||
resolveChunkDebugInfo(streamState, blockedChunk);
|
||||
}
|
||||
promise.then(
|
||||
() => resolveModuleChunk(response, blockedChunk, clientReference),
|
||||
@@ -2974,12 +2952,12 @@ function resolveModule(
|
||||
if (!chunk) {
|
||||
const newChunk = createResolvedModuleChunk(response, clientReference);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
// This can't actually happen because we don't have any forward
|
||||
// references to modules.
|
||||
@@ -3000,13 +2978,13 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
|
||||
if (!chunk) {
|
||||
const newChunk = createInitializedStreamChunk(response, stream, controller);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
return;
|
||||
}
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
if (chunk.status !== PENDING) {
|
||||
// We already resolved. We didn't expect to see this.
|
||||
@@ -3056,10 +3034,6 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3459,12 +3433,12 @@ function resolvePostponeDev(
|
||||
postponeInstance,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
@@ -3493,12 +3467,12 @@ function resolveErrorModel(
|
||||
errorWithDigest,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, errorWithDigest);
|
||||
}
|
||||
@@ -4344,26 +4318,7 @@ function flushComponentPerformance(
|
||||
|
||||
// First find the start time of the first component to know if it was running
|
||||
// in parallel with the previous.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
const debugInfo = __DEV__ && root._debugInfo;
|
||||
if (debugInfo) {
|
||||
let startTime = 0;
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
|
||||
@@ -102,7 +102,6 @@ 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) {
|
||||
@@ -111,10 +110,9 @@ export function logComponentRender(
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0, '');
|
||||
}
|
||||
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, measureName, {
|
||||
performance.measure.bind(performance, '\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
@@ -127,10 +125,9 @@ export function logComponentRender(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
measureName,
|
||||
'\u200b' + entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
childrenEndTime,
|
||||
trackNames[trackIdx],
|
||||
@@ -155,7 +152,6 @@ 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]> = [
|
||||
[
|
||||
@@ -169,8 +165,7 @@ export function logComponentAborted(
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0, '');
|
||||
}
|
||||
|
||||
performance.measure(measureName, {
|
||||
performance.measure('\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
@@ -183,10 +178,9 @@ export function logComponentAborted(
|
||||
},
|
||||
},
|
||||
});
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
measureName,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
childrenEndTime,
|
||||
trackNames[trackIdx],
|
||||
@@ -212,7 +206,6 @@ 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' &&
|
||||
@@ -229,8 +222,7 @@ export function logComponentErrored(
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0, '');
|
||||
}
|
||||
|
||||
performance.measure(measureName, {
|
||||
performance.measure('\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
@@ -243,10 +235,9 @@ export function logComponentErrored(
|
||||
},
|
||||
},
|
||||
});
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
measureName,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
childrenEndTime,
|
||||
trackNames[trackIdx],
|
||||
@@ -406,7 +397,6 @@ export function logComponentAwaitAborted(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(entryName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
@@ -463,7 +453,6 @@ export function logComponentAwaitErrored(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(entryName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
@@ -525,7 +514,6 @@ export function logComponentAwait(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(entryName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
@@ -550,7 +538,6 @@ 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' &&
|
||||
@@ -563,10 +550,9 @@ 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, measureName, {
|
||||
performance.measure.bind(performance, '\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
@@ -579,10 +565,9 @@ export function logIOInfoErrored(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
measureName,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
@@ -605,7 +590,6 @@ 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) {
|
||||
@@ -621,7 +605,7 @@ export function logIOInfo(
|
||||
);
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, measureName, {
|
||||
performance.measure.bind(performance, '\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
@@ -634,10 +618,9 @@ export function logIOInfo(
|
||||
},
|
||||
}),
|
||||
);
|
||||
performance.clearMeasures(measureName);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
measureName,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-core",
|
||||
"version": "7.0.0",
|
||||
"version": "6.1.5",
|
||||
"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": "7.0.0",
|
||||
"version_name": "7.0.0",
|
||||
"version": "6.1.5",
|
||||
"version_name": "6.1.5",
|
||||
"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": "7.0.0",
|
||||
"version_name": "7.0.0",
|
||||
"version": "6.1.5",
|
||||
"version_name": "6.1.5",
|
||||
"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": "7.0.0",
|
||||
"version": "6.1.5",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "@react-devtools",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-inline",
|
||||
"version": "7.0.0",
|
||||
"version": "6.1.5",
|
||||
"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).id;
|
||||
const deepestedNodeID = agent.getIDForHostInstance(ref.current);
|
||||
|
||||
await act(() => store.toggleIsCollapsed(deepestedNodeID, false));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
@@ -3142,105 +3142,4 @@ 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,8 +251,7 @@ describe('Store component filters', () => {
|
||||
});
|
||||
|
||||
it('should filter ViewTransition', async () => {
|
||||
const ViewTransition =
|
||||
React.ViewTransition || React.unstable_ViewTransition;
|
||||
const ViewTransition = React.unstable_ViewTransition;
|
||||
|
||||
if (ViewTransition != null) {
|
||||
await actAsync(async () =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user