Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
663ddab596 | ||
|
|
408b38ef73 | ||
|
|
09056abde7 | ||
|
|
c91783c1f2 | ||
|
|
e0654becf7 | ||
|
|
6160773f30 | ||
|
|
eb2f784e75 | ||
|
|
723b25c644 | ||
|
|
bbb7a1fdf7 | ||
|
|
6b344c7c53 | ||
|
|
71b3a03cc9 | ||
|
|
39c6545cef | ||
|
|
613cf80f26 | ||
|
|
ea0c17b095 | ||
|
|
031595d720 | ||
|
|
3cde211b0c | ||
|
|
1d3664665b | ||
|
|
2bcbf254f1 | ||
|
|
aaad0ea055 | ||
|
|
02c80f0d87 | ||
|
|
21272a680f | ||
|
|
1440f4f42d | ||
|
|
f6a4882859 | ||
|
|
b485f7cf64 | ||
|
|
2cfb221937 | ||
|
|
58bdc0bb96 | ||
|
|
bf11d2fb2f | ||
|
|
ec7d9a7249 | ||
|
|
40c7a7f6ca |
@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '7.0.0',
|
||||
'eslint-plugin-react-hooks': '7.1.0',
|
||||
'jest-react': '0.18.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
|
||||
@@ -14,6 +14,7 @@ import React, {
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
Activity,
|
||||
} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
@@ -34,12 +35,8 @@ export default function ConfigEditor({
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
// TODO: Use <Activity> when it is compatible with Monaco: https://github.com/suren-atoyan/monaco-react/issues/753
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<Activity mode={isExpanded ? 'visible' : 'hidden'}>
|
||||
<ExpandedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
@@ -49,11 +46,8 @@ export default function ConfigEditor({
|
||||
}}
|
||||
formattedAppliedConfig={formattedAppliedConfig}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: !isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
</Activity>
|
||||
<Activity mode={isExpanded ? 'hidden' : 'visible'}>
|
||||
<CollapsedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
@@ -62,7 +56,7 @@ export default function ConfigEditor({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Activity>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -122,7 +116,8 @@ function ExpandedEditor({
|
||||
|
||||
return (
|
||||
<ViewTransition
|
||||
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
|
||||
enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
|
||||
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}>
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@babel/traverse": "^7.18.9",
|
||||
"@babel/types": "7.26.3",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@monaco-editor/react": "^4.8.0-rc.2",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@use-gesture/react": "^10.2.22",
|
||||
"hermes-eslint": "^0.25.0",
|
||||
@@ -40,13 +40,13 @@
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-format": "^29.3.1",
|
||||
"re-resizable": "^6.9.16",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
"react": "19.2",
|
||||
"react-dom": "19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/react": "19.2",
|
||||
"@types/react-dom": "19.2",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
"concurrently": "^7.4.0",
|
||||
@@ -58,7 +58,7 @@
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9"
|
||||
"@types/react": "19.2",
|
||||
"@types/react-dom": "19.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,15 @@
|
||||
::view-transition-group(.slide-in) {
|
||||
z-index: 1;
|
||||
}
|
||||
::view-transition-old(.slide-out) {
|
||||
animation-name: slideOutLeft;
|
||||
}
|
||||
::view-transition-new(.slide-out) {
|
||||
animation-name: slideInLeft;
|
||||
}
|
||||
::view-transition-group(.slide-out) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes slideOutLeft {
|
||||
from {
|
||||
|
||||
@@ -701,19 +701,19 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@monaco-editor/loader@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
|
||||
integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==
|
||||
"@monaco-editor/loader@^1.6.1":
|
||||
version "1.6.1"
|
||||
resolved "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz#c99177d87765abf10de31a0086084e714acfbc0f"
|
||||
integrity sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==
|
||||
dependencies:
|
||||
state-local "^1.0.6"
|
||||
|
||||
"@monaco-editor/react@^4.4.6":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119"
|
||||
integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==
|
||||
"@monaco-editor/react@^4.8.0-rc.2":
|
||||
version "4.8.0-rc.2"
|
||||
resolved "https://registry.npmjs.org/@monaco-editor/react/-/react-4.8.0-rc.2.tgz#e9acf652e23e9f640671a69875f496dde7f098aa"
|
||||
integrity sha512-RzFHKBCnRA4RnozaG/EPhKsbkhX5wcApSa5MElR/AD2ojxhMY+QP+G8aJpxALCnIwKs6L0dec5MJ0nAjMUEqnA==
|
||||
dependencies:
|
||||
"@monaco-editor/loader" "^1.4.0"
|
||||
"@monaco-editor/loader" "^1.6.1"
|
||||
|
||||
"@next/env@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
@@ -859,6 +859,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b"
|
||||
integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==
|
||||
|
||||
"@types/react-dom@19.2":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz#a4cc874797b7ddc9cb180ef0d5dc23f596fc2332"
|
||||
integrity sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==
|
||||
|
||||
"@types/react@19.1.12":
|
||||
version "19.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.12.tgz#7bfaa76aabbb0b4fe0493c21a3a7a93d33e8937b"
|
||||
@@ -866,10 +871,10 @@
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@19.1.13":
|
||||
version "19.1.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883"
|
||||
integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==
|
||||
"@types/react@19.2":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36"
|
||||
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
@@ -3589,12 +3594,12 @@ re-resizable@^6.9.16:
|
||||
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.10.0.tgz#d684a096ab438f1a93f59ad3a580a206b0ce31ee"
|
||||
integrity sha512-hysSK0xmA5nz24HBVztlk4yCqCLCvS32E6ZpWxVKop9x3tqCa4yAj1++facrmkOf62JsJHjmjABdKxXofYioCw==
|
||||
|
||||
react-dom@19.1.1:
|
||||
version "19.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893"
|
||||
integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==
|
||||
react-dom@19.2:
|
||||
version "19.2.0"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8"
|
||||
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
|
||||
dependencies:
|
||||
scheduler "^0.26.0"
|
||||
scheduler "^0.27.0"
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
@@ -3606,10 +3611,10 @@ react-is@^18.0.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
react@19.1.1:
|
||||
version "19.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af"
|
||||
integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==
|
||||
react@19.2:
|
||||
version "19.2.0"
|
||||
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5"
|
||||
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
|
||||
|
||||
read-cache@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -3785,10 +3790,10 @@ safe-regex-test@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
is-regex "^1.2.1"
|
||||
|
||||
scheduler@^0.26.0:
|
||||
version "0.26.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
|
||||
integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
|
||||
scheduler@^0.27.0:
|
||||
version "0.27.0"
|
||||
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
|
||||
integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
|
||||
|
||||
semver@^6.3.1:
|
||||
version "6.3.1"
|
||||
|
||||
@@ -12,6 +12,28 @@ import {Err, Ok, Result} from './Utils/Result';
|
||||
import {assertExhaustive} from './Utils/utils';
|
||||
import invariant from 'invariant';
|
||||
|
||||
// Number of context lines to display above the source of an error
|
||||
const CODEFRAME_LINES_ABOVE = 2;
|
||||
// Number of context lines to display below the source of an error
|
||||
const CODEFRAME_LINES_BELOW = 3;
|
||||
/*
|
||||
* Max number of lines for the _source_ of an error, before we abbreviate
|
||||
* the display of the source portion
|
||||
*/
|
||||
const CODEFRAME_MAX_LINES = 10;
|
||||
/*
|
||||
* When the error source exceeds the above threshold, how many lines of
|
||||
* the source should be displayed? We show:
|
||||
* - CODEFRAME_LINES_ABOVE context lines
|
||||
* - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
|
||||
* - '...' ellipsis
|
||||
* - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
|
||||
* - CODEFRAME_LINES_BELOW context lines
|
||||
*
|
||||
* This value must be at least 2 or else we'll cut off important parts of the error message
|
||||
*/
|
||||
const CODEFRAME_ABBREVIATED_SOURCE_LINES = 5;
|
||||
|
||||
export enum ErrorSeverity {
|
||||
/**
|
||||
* An actionable error that the developer can fix. For example, product code errors should be
|
||||
@@ -496,7 +518,7 @@ function printCodeFrame(
|
||||
loc: t.SourceLocation,
|
||||
message: string,
|
||||
): string {
|
||||
return codeFrameColumns(
|
||||
const printed = codeFrameColumns(
|
||||
source,
|
||||
{
|
||||
start: {
|
||||
@@ -510,8 +532,25 @@ function printCodeFrame(
|
||||
},
|
||||
{
|
||||
message,
|
||||
linesAbove: CODEFRAME_LINES_ABOVE,
|
||||
linesBelow: CODEFRAME_LINES_BELOW,
|
||||
},
|
||||
);
|
||||
const lines = printed.split(/\r?\n/);
|
||||
if (loc.end.line - loc.start.line < CODEFRAME_MAX_LINES) {
|
||||
return printed;
|
||||
}
|
||||
const pipeIndex = lines[0].indexOf('|');
|
||||
return [
|
||||
...lines.slice(
|
||||
0,
|
||||
CODEFRAME_LINES_ABOVE + CODEFRAME_ABBREVIATED_SOURCE_LINES,
|
||||
),
|
||||
' '.repeat(pipeIndex) + '…',
|
||||
...lines.slice(
|
||||
-(CODEFRAME_LINES_BELOW + CODEFRAME_ABBREVIATED_SOURCE_LINES),
|
||||
),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
|
||||
@@ -103,6 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
@@ -275,6 +276,10 @@ function runWithEnvironment(
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects_exp) {
|
||||
validateNoDerivedComputationsInEffects_exp(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir, env));
|
||||
}
|
||||
|
||||
@@ -1568,20 +1568,6 @@ function lowerObjectPropertyKey(
|
||||
name: key.node.value,
|
||||
};
|
||||
} else if (property.node.computed && key.isExpression()) {
|
||||
if (!key.isIdentifier() && !key.isMemberExpression()) {
|
||||
/*
|
||||
* NOTE: allowing complex key expressions can trigger a bug where a mutation is made conditional
|
||||
* see fixture
|
||||
* error.object-expression-computed-key-modified-during-after-construction.js
|
||||
*/
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: key.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const place = lowerExpressionToTemporary(builder, key);
|
||||
return {
|
||||
kind: 'computed',
|
||||
|
||||
@@ -324,6 +324,12 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoDerivedComputationsInEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Experimental: Validates that effects are not used to calculate derived data which could instead be computed
|
||||
* during render. Generates a custom error message for each type of violation.
|
||||
*/
|
||||
validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against creating JSX within a try block and recommends using an error boundary
|
||||
* instead.
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
Place,
|
||||
CallExpression,
|
||||
Instruction,
|
||||
isUseStateType,
|
||||
BasicBlock,
|
||||
isUseRefType,
|
||||
GeneratedSource,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sourcesIds: Set<IdentifierId>;
|
||||
};
|
||||
|
||||
type ValidationContext = {
|
||||
readonly functions: Map<IdentifierId, FunctionExpression>;
|
||||
readonly errors: CompilerError;
|
||||
readonly derivationCache: DerivationCache;
|
||||
readonly effects: Set<HIRFunction>;
|
||||
readonly setStateCache: Map<string | undefined | null, Array<Place>>;
|
||||
readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
|
||||
};
|
||||
|
||||
class DerivationCache {
|
||||
hasChanges: boolean = false;
|
||||
cache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
private previousCache: Map<IdentifierId, DerivationMetadata> | null = null;
|
||||
|
||||
takeSnapshot(): void {
|
||||
this.previousCache = new Map();
|
||||
for (const [key, value] of this.cache.entries()) {
|
||||
this.previousCache.set(key, {
|
||||
place: value.place,
|
||||
sourcesIds: new Set(value.sourcesIds),
|
||||
typeOfValue: value.typeOfValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkForChanges(): void {
|
||||
if (this.previousCache === null) {
|
||||
this.hasChanges = true;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of this.cache.entries()) {
|
||||
const previousValue = this.previousCache.get(key);
|
||||
if (
|
||||
previousValue === undefined ||
|
||||
!this.isDerivationEqual(previousValue, value)
|
||||
) {
|
||||
this.hasChanges = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.cache.size !== this.previousCache.size) {
|
||||
this.hasChanges = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasChanges = false;
|
||||
}
|
||||
|
||||
snapshot(): boolean {
|
||||
const hasChanges = this.hasChanges;
|
||||
this.hasChanges = false;
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
addDerivationEntry(
|
||||
derivedVar: Place,
|
||||
sourcesIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: derivedVar,
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sourcesIds !== undefined) {
|
||||
for (const id of sourcesIds) {
|
||||
const sourcePlace = this.cache.get(id)?.place;
|
||||
|
||||
if (sourcePlace === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
if (
|
||||
sourcePlace.identifier.name === null ||
|
||||
sourcePlace.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
} else {
|
||||
newValue.sourcesIds.add(sourcePlace.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue.sourcesIds.size === 0) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
}
|
||||
|
||||
this.cache.set(derivedVar.identifier.id, newValue);
|
||||
}
|
||||
|
||||
private isDerivationEqual(
|
||||
a: DerivationMetadata,
|
||||
b: DerivationMetadata,
|
||||
): boolean {
|
||||
if (a.typeOfValue !== b.typeOfValue) {
|
||||
return false;
|
||||
}
|
||||
if (a.sourcesIds.size !== b.sourcesIds.size) {
|
||||
return false;
|
||||
}
|
||||
for (const id of a.sourcesIds) {
|
||||
if (!b.sourcesIds.has(id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
*
|
||||
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // 🔴 Avoid: redundant state and unnecessary Effect
|
||||
* const [fullName, setFullName] = useState('');
|
||||
* useEffect(() => {
|
||||
* setFullName(firstName + ' ' + lastName);
|
||||
* }, [firstName, lastName]);
|
||||
* ```
|
||||
*
|
||||
* Instead use:
|
||||
*
|
||||
* ```
|
||||
* // ✅ Good: calculated during rendering
|
||||
* const fullName = firstName + ' ' + lastName;
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects_exp(
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const derivationCache = new DerivationCache();
|
||||
const errors = new CompilerError();
|
||||
const effects: Set<HIRFunction> = new Set();
|
||||
|
||||
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
|
||||
const effectSetStateCache: Map<
|
||||
string | undefined | null,
|
||||
Array<Place>
|
||||
> = new Map();
|
||||
|
||||
const context: ValidationContext = {
|
||||
functions,
|
||||
errors,
|
||||
derivationCache,
|
||||
effects,
|
||||
setStateCache,
|
||||
effectSetStateCache,
|
||||
};
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let isFirstPass = true;
|
||||
do {
|
||||
context.derivationCache.takeSnapshot();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
recordPhiDerivations(block, context);
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context, isFirstPass);
|
||||
}
|
||||
}
|
||||
|
||||
context.derivationCache.checkForChanges();
|
||||
isFirstPass = false;
|
||||
} while (context.derivationCache.snapshot());
|
||||
|
||||
for (const effect of effects) {
|
||||
validateEffect(effect, context);
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function recordPhiDerivations(
|
||||
block: BasicBlock,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
for (const phi of block.phis) {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let sourcesIds: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const operandMetadata = context.derivationCache.cache.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
sourcesIds.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
context.derivationCache.addDerivationEntry(
|
||||
phi.place,
|
||||
sourcesIds,
|
||||
typeOfValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function joinValue(
|
||||
lvalueType: TypeOfValue,
|
||||
valueType: TypeOfValue,
|
||||
): TypeOfValue {
|
||||
if (lvalueType === 'ignored') return valueType;
|
||||
if (valueType === 'ignored') return lvalueType;
|
||||
if (lvalueType === valueType) return lvalueType;
|
||||
return 'fromPropsAndState';
|
||||
}
|
||||
|
||||
function recordInstructionDerivations(
|
||||
instr: Instruction,
|
||||
context: ValidationContext,
|
||||
isFirstPass: boolean,
|
||||
): void {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
context.functions.set(lvalue.identifier.id, value);
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context, isFirstPass);
|
||||
}
|
||||
}
|
||||
} else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = context.functions.get(value.args[0].identifier.id);
|
||||
if (effectFunction != null) {
|
||||
context.effects.add(effectFunction.loweredFunc.func);
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
|
||||
const stateValueSource = value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.add(stateValueSource.identifier.id);
|
||||
}
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource &&
|
||||
isFirstPass
|
||||
) {
|
||||
if (context.setStateCache.has(operand.loc.identifierName)) {
|
||||
context.setStateCache.get(operand.loc.identifierName)!.push(operand);
|
||||
} else {
|
||||
context.setStateCache.set(operand.loc.identifierName, [operand]);
|
||||
}
|
||||
}
|
||||
|
||||
const operandMetadata = context.derivationCache.cache.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
for (const id of operandMetadata.sourcesIds) {
|
||||
sources.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
context.derivationCache.addDerivationEntry(lvalue, sources, typeOfValue);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
switch (operand.effect) {
|
||||
case Effect.Capture:
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
if (isMutable(instr, operand)) {
|
||||
if (context.derivationCache.cache.has(operand.identifier.id)) {
|
||||
const operandMetadata = context.derivationCache.cache.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
|
||||
if (operandMetadata !== undefined) {
|
||||
operandMetadata.typeOfValue = joinValue(
|
||||
typeOfValue,
|
||||
operandMetadata.typeOfValue,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
context.derivationCache.addDerivationEntry(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
|
||||
const effectDerivedSetStateCalls: Array<{
|
||||
value: CallExpression;
|
||||
loc: SourceLocation;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
typeOfValue: TypeOfValue;
|
||||
}> = [];
|
||||
|
||||
const globals: Set<IdentifierId> = new Set();
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// skip if block has a back edge
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
// Early return if any instruction is deriving a value from a ref
|
||||
if (isUseRefType(instr.lvalue.identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource
|
||||
) {
|
||||
if (context.effectSetStateCache.has(operand.loc.identifierName)) {
|
||||
context.effectSetStateCache
|
||||
.get(operand.loc.identifierName)!
|
||||
.push(operand);
|
||||
} else {
|
||||
context.effectSetStateCache.set(operand.loc.identifierName, [
|
||||
operand,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const argMetadata = context.derivationCache.cache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: instr.value,
|
||||
loc: instr.value.callee.loc,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
typeOfValue: argMetadata.typeOfValue,
|
||||
});
|
||||
}
|
||||
} else if (instr.value.kind === 'CallExpression') {
|
||||
const calleeMetadata = context.derivationCache.cache.get(
|
||||
instr.value.callee.identifier.id,
|
||||
);
|
||||
|
||||
if (
|
||||
calleeMetadata !== undefined &&
|
||||
(calleeMetadata.typeOfValue === 'fromProps' ||
|
||||
calleeMetadata.typeOfValue === 'fromPropsAndState')
|
||||
) {
|
||||
// If the callee is a prop we can't confidently say that it should be derived in render
|
||||
return;
|
||||
}
|
||||
|
||||
if (globals.has(instr.value.callee.identifier.id)) {
|
||||
// If the callee is a global we can't confidently say that it should be derived in render
|
||||
return;
|
||||
}
|
||||
} else if (instr.value.kind === 'LoadGlobal') {
|
||||
globals.add(instr.lvalue.identifier.id);
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
globals.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
|
||||
if (
|
||||
derivedSetStateCall.loc !== GeneratedSource &&
|
||||
context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
|
||||
.length ===
|
||||
context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
|
||||
.length -
|
||||
1
|
||||
) {
|
||||
const derivedDepsStr = Array.from(derivedSetStateCall.sourceIds)
|
||||
.map(sourceId => {
|
||||
const sourceMetadata = context.derivationCache.cache.get(sourceId);
|
||||
return sourceMetadata?.place.identifier.name?.value;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
let description;
|
||||
|
||||
if (derivedSetStateCall.typeOfValue === 'fromProps') {
|
||||
description = `From props: [${derivedDepsStr}]`;
|
||||
} else if (derivedSetStateCall.typeOfValue === 'fromState') {
|
||||
description = `From local state: [${derivedDepsStr}]`;
|
||||
} else {
|
||||
description = `From props and local state: [${derivedDepsStr}]`;
|
||||
}
|
||||
|
||||
context.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
description: `Derived values (${description}) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user`,
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'You might not need an effect. Derive values in render, not effects.',
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: derivedSetStateCall.value.callee.loc,
|
||||
message: 'This should be computed during render, not in an effect',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { initialName } = t0;
|
||||
const [name, setName] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== initialName) {
|
||||
t1 = () => {
|
||||
setName(initialName);
|
||||
};
|
||||
t2 = [initialName];
|
||||
$[0] = initialName;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = (e) => setName(e.target.value);
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== name) {
|
||||
t4 = (
|
||||
<div>
|
||||
<input value={name} onChange={t3} />
|
||||
</div>
|
||||
);
|
||||
$[4] = name;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ initialName: "John" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="John"></div>
|
||||
@@ -0,0 +1,21 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
|
||||
}
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
return <MockComponent onSet={setValue} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function MockComponent(t0) {
|
||||
const $ = _c(2);
|
||||
const { onSet } = t0;
|
||||
let t1;
|
||||
if ($[0] !== onSet) {
|
||||
t1 = <div onClick={() => onSet("clicked")}>Mock Component</div>;
|
||||
$[0] = onSet;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const { propValue } = t0;
|
||||
const [, setValue] = useState(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== propValue) {
|
||||
t1 = () => {
|
||||
setValue(propValue);
|
||||
};
|
||||
t2 = [propValue];
|
||||
$[0] = propValue;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <MockComponent onSet={setValue} />;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Mock Component</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
|
||||
}
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
return <MockComponent onSet={setValue} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(myRef.current + test);
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState("");
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
setLocal(myRef.current + test);
|
||||
};
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: "testString" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) nulltestString
|
||||
@@ -0,0 +1,19 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(myRef.current + test);
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
onChange();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test', onChange: () => {}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { propValue, onChange } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
if ($[0] !== onChange || $[1] !== propValue) {
|
||||
t1 = () => {
|
||||
setValue(propValue);
|
||||
onChange();
|
||||
};
|
||||
$[0] = onChange;
|
||||
$[1] = propValue;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== propValue) {
|
||||
t2 = [propValue];
|
||||
$[3] = propValue;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== value) {
|
||||
t3 = <div>{value}</div>;
|
||||
$[5] = value;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test", onChange: () => {} }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
onChange();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test', onChange: () => {}}],
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { propValue } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== propValue) {
|
||||
t1 = () => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
};
|
||||
t2 = [propValue];
|
||||
$[0] = propValue;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== value) {
|
||||
t3 = <div>{value}</div>;
|
||||
$[3] = value;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) globalCall is not defined
|
||||
@@ -0,0 +1,17 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [value]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-conditionally-in-effect.ts:9:6
|
||||
7 | useEffect(() => {
|
||||
8 | if (enabled) {
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | } else {
|
||||
11 | setLocalValue('disabled');
|
||||
12 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [input]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-from-default-props.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setCurrInput(input + localConst);
|
||||
| ^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [input, localConst]);
|
||||
11 |
|
||||
12 | return <div>{currInput}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: [count]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-from-local-state-in-effect.ts:10:6
|
||||
8 | useEffect(() => {
|
||||
9 | if (shouldChange) {
|
||||
> 10 | setCount(count + 1);
|
||||
| ^^^^^^^^ This should be computed during render, not in an effect
|
||||
11 | }
|
||||
12 | }, [count]);
|
||||
13 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props and local state: [firstName, lastName]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-from-prop-local-state-and-component-scope.ts:11:4
|
||||
9 |
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
12 | }, [firstName, middleName, lastName]);
|
||||
13 |
|
||||
14 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [value]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-from-prop-with-side-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
9 | document.title = `Value: ${value}`;
|
||||
10 | }, [value]);
|
||||
11 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [propValue]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.effect-contains-local-function-call.ts:12:4
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
> 12 | setValue(propValue);
|
||||
| ^^^^^^^^ This should be computed during render, not in an effect
|
||||
13 | localFunction();
|
||||
14 | }, [propValue]);
|
||||
15 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: [firstName]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [props]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-computed-props.ts:9:4
|
||||
7 | useEffect(() => {
|
||||
8 | const computed = props.prefix + props.value + props.suffix;
|
||||
> 9 | setDisplayValue(computed);
|
||||
| ^^^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [props.prefix, props.value, props.suffix]);
|
||||
11 |
|
||||
12 | return <div>{displayValue}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [props]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-destructured-props.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(props.firstName + ' ' + props.lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
11 | }, [props.firstName, props.lastName]);
|
||||
12 |
|
||||
13 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState(0);
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (myRef.current) {
|
||||
setLocal(test);
|
||||
} else {
|
||||
setLocal(test + test);
|
||||
}
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 4}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState(0);
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
if (myRef.current) {
|
||||
setLocal(test);
|
||||
} else {
|
||||
setLocal(test + test);
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: 4 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 8
|
||||
@@ -0,0 +1,23 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState(0);
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (myRef.current) {
|
||||
setLocal(test);
|
||||
} else {
|
||||
setLocal(test + test);
|
||||
}
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 4}],
|
||||
};
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function BadExample() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
@@ -10,7 +12,7 @@ function BadExample() {
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(capitalize(firstName + ' ' + lastName));
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
@@ -26,14 +28,14 @@ Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
error.invalid-derived-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function BadExample() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
@@ -6,7 +8,7 @@ function BadExample() {
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(capitalize(firstName + ' ' + lastName));
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
|
||||
@@ -60,29 +60,7 @@ This argument is a function which may reassign or mutate `cache` after render, w
|
||||
> 22 | // The original issue is that `cache` was not memoized together with the returned
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 23 | // function. This was because neither appears to ever be mutated — the function
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | // is known to mutate `cache` but the function isn't called.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 25 | //
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 26 | // The fix is to detect cases like this — functions that are mutable but not called -
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 27 | // and ensure that their mutable captures are aliased together into the same scope.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 28 | const cache = new WeakMap<TInput, TOutput>();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 29 | return input => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 30 | let output = cache.get(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 31 | if (output == null) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 32 | output = map(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 33 | cache.set(input, output);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 34 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
…
|
||||
> 35 | return output;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 36 | };
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[(mutate(key), key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerExpression) Expected Identifier, got SequenceExpression key in ObjectExpression
|
||||
|
||||
error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr.ts:6:6
|
||||
4 | const key = {};
|
||||
5 | const context = {
|
||||
> 6 | [(mutate(key), key)]: identity([props.value]),
|
||||
| ^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got SequenceExpression key in ObjectExpression
|
||||
7 | };
|
||||
8 | mutate(key);
|
||||
9 | return context;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
|
||||
error.todo-object-expression-computed-key-modified-during-after-construction.ts:6:5
|
||||
4 | const key = {};
|
||||
5 | const context = {
|
||||
> 6 | [mutateAndReturn(key)]: identity([props.value]),
|
||||
| ^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
7 | };
|
||||
8 | mutate(key);
|
||||
9 | return context;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
|
||||
error.todo-object-expression-computed-key-mutate-key-while-constructing-object.ts:6:5
|
||||
4 | const key = {};
|
||||
5 | const context = {
|
||||
> 6 | [mutateAndReturn(key)]: identity([props.value]),
|
||||
| ^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
7 | };
|
||||
8 | return context;
|
||||
9 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const obj = {mutateAndReturn};
|
||||
const key = {};
|
||||
const context = {
|
||||
[obj.mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Todo: (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
|
||||
error.todo-object-expression-member-expr-call.ts:7:5
|
||||
5 | const key = {};
|
||||
6 | const context = {
|
||||
> 7 | [obj.mutateAndReturn(key)]: identity([props.value]),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Expected Identifier, got CallExpression key in ObjectExpression
|
||||
8 | };
|
||||
9 | mutate(key);
|
||||
10 | return context;
|
||||
```
|
||||
|
||||
|
||||
@@ -64,20 +64,7 @@ error.todo-preserve-memo-deps-mixed-optional-nonoptional-property-chain.ts:7:25
|
||||
> 8 | return identity({
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | callback: () => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 10 | // This is a bug in our dependency inference: we stop capturing dependencies
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 11 | // after x.a.b?.c. But what this dependency is telling us is that if `x.a.b`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 12 | // was non-nullish, then we can access `.c.d?.e`. Thus we should take the
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 13 | // full property chain, exactly as-is with optionals/non-optionals, as a
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 14 | // dependency
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 15 | return identity(x.a.b?.c.d?.e);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 16 | },
|
||||
…
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 17 | });
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -46,18 +46,7 @@ error.todo-syntax.ts:11:2
|
||||
> 12 | () => {
|
||||
| ^^^^^^^^^^^
|
||||
> 13 | try {
|
||||
| ^^^^^^^^^^^
|
||||
> 14 | console.log(prop1);
|
||||
| ^^^^^^^^^^^
|
||||
> 15 | } finally {
|
||||
| ^^^^^^^^^^^
|
||||
> 16 | console.log('exiting');
|
||||
| ^^^^^^^^^^^
|
||||
> 17 | }
|
||||
| ^^^^^^^^^^^
|
||||
> 18 | },
|
||||
| ^^^^^^^^^^^
|
||||
> 19 | [prop1],
|
||||
…
|
||||
| ^^^^^^^^^^^
|
||||
> 20 | AUTODEPS
|
||||
| ^^^^^^^^^^^
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[(mutate(key), key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, mutate, mutateAndReturn } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.value) {
|
||||
const key = {};
|
||||
const context = { [(mutate(key), key)]: identity([props.value]) };
|
||||
mutate(key);
|
||||
t0 = [context, key];
|
||||
$[0] = props.value;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [{ value: 42 }, { value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
[{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
@@ -6,10 +6,11 @@ function Component(props) {
|
||||
[(mutate(key), key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, mutate, mutateAndReturn } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let context;
|
||||
if ($[0] !== props.value) {
|
||||
const key = {};
|
||||
context = { [mutateAndReturn(key)]: identity([props.value]) };
|
||||
mutate(key);
|
||||
$[0] = props.value;
|
||||
$[1] = context;
|
||||
} else {
|
||||
context = $[1];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [{ value: 42 }, { value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) {"[object Object]":[42]}
|
||||
{"[object Object]":[42]}
|
||||
@@ -12,4 +12,5 @@ function Component(props) {
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const context = {
|
||||
[mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, mutate, mutateAndReturn } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const key = {};
|
||||
|
||||
t0 = mutateAndReturn(key);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
let t1;
|
||||
if ($[1] !== props.value) {
|
||||
t1 = identity([props.value]);
|
||||
$[1] = props.value;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = { [t0]: t1 };
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const context = t2;
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) {"[object Object]":[42]}
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, mutate, mutateAndReturn} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const obj = {mutateAndReturn};
|
||||
const key = {};
|
||||
const context = {
|
||||
[obj.mutateAndReturn(key)]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, mutate, mutateAndReturn } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let context;
|
||||
if ($[0] !== props.value) {
|
||||
const obj = { mutateAndReturn };
|
||||
const key = {};
|
||||
context = { [obj.mutateAndReturn(key)]: identity([props.value]) };
|
||||
mutate(key);
|
||||
$[0] = props.value;
|
||||
$[1] = context;
|
||||
} else {
|
||||
context = $[1];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) {"[object Object]":[42]}
|
||||
@@ -29,10 +29,13 @@ export {
|
||||
ProgramContext,
|
||||
tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization,
|
||||
findDirectiveDisablingMemoization,
|
||||
defaultOptions,
|
||||
type CompilerPipelineValue,
|
||||
type Logger,
|
||||
type LoggerEvent,
|
||||
type PluginOptions,
|
||||
type AutoDepsDecorationsEvent,
|
||||
type CompileSuccessEvent,
|
||||
} from './Entrypoint';
|
||||
export {
|
||||
Effect,
|
||||
|
||||
@@ -5,15 +5,22 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('no impure function calls rule', ReactCompilerRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Known impure function calls are caught',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'no impure function calls rule',
|
||||
allRules[getRuleForCategory(ErrorCategory.Purity).name].rule,
|
||||
{
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Known impure function calls are caught',
|
||||
code: normalizeIndent`
|
||||
function Component() {
|
||||
const date = Date.now();
|
||||
const now = performance.now();
|
||||
@@ -21,11 +28,12 @@ testRule('no impure function calls rule', ReactCompilerRule, {
|
||||
return <Foo date={date} now={now} rand={rand} />;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,23 +5,30 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, makeTestCaseError, testRule} from './shared-utils';
|
||||
import {AllRules} from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('rules-of-hooks', AllRules, {
|
||||
valid: [
|
||||
{
|
||||
name: 'Basic example',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'rules-of-hooks',
|
||||
allRules[getRuleForCategory(ErrorCategory.Hooks).name].rule,
|
||||
{
|
||||
valid: [
|
||||
{
|
||||
name: 'Basic example',
|
||||
code: normalizeIndent`
|
||||
function Component() {
|
||||
useHook();
|
||||
return <div>Hello world</div>;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Violation with Flow suppression',
|
||||
code: `
|
||||
},
|
||||
{
|
||||
name: 'Violation with Flow suppression',
|
||||
code: `
|
||||
// Valid since error already suppressed with flow.
|
||||
function useHook() {
|
||||
if (cond) {
|
||||
@@ -30,11 +37,11 @@ testRule('rules-of-hooks', AllRules, {
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
// OK because invariants are only meant for the compiler team's consumption
|
||||
name: '[Invariant] Defined after use',
|
||||
code: normalizeIndent`
|
||||
},
|
||||
{
|
||||
// OK because invariants are only meant for the compiler team's consumption
|
||||
name: '[Invariant] Defined after use',
|
||||
code: normalizeIndent`
|
||||
function Component(props) {
|
||||
let y = function () {
|
||||
m(x);
|
||||
@@ -45,42 +52,49 @@ testRule('rules-of-hooks', AllRules, {
|
||||
return y;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Classes don't throw",
|
||||
code: normalizeIndent`
|
||||
},
|
||||
{
|
||||
name: "Classes don't throw",
|
||||
code: normalizeIndent`
|
||||
class Foo {
|
||||
#bar() {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Simple violation',
|
||||
code: normalizeIndent`
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Simple violation',
|
||||
code: normalizeIndent`
|
||||
function useConditional() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Hooks must always be called in a consistent order'),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Multiple diagnostics within the same function are surfaced',
|
||||
code: normalizeIndent`
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Hooks must always be called in a consistent order',
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Multiple diagnostics within the same function are surfaced',
|
||||
code: normalizeIndent`
|
||||
function useConditional() {
|
||||
cond ?? useConditionalHook();
|
||||
props.cond && useConditionalHook();
|
||||
return <div>Hello world</div>;
|
||||
}`,
|
||||
errors: [
|
||||
makeTestCaseError('Hooks must always be called in a consistent order'),
|
||||
makeTestCaseError('Hooks must always be called in a consistent order'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Hooks must always be called in a consistent order',
|
||||
),
|
||||
makeTestCaseError(
|
||||
'Hooks must always be called in a consistent order',
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,15 +5,22 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('no ambiguous JSX rule', ReactCompilerRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'JSX in try blocks are warned against',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'no ambiguous JSX rule',
|
||||
allRules[getRuleForCategory(ErrorCategory.ErrorBoundaries).name].rule,
|
||||
{
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'JSX in try blocks are warned against',
|
||||
code: normalizeIndent`
|
||||
function Component(props) {
|
||||
let el;
|
||||
try {
|
||||
@@ -24,7 +31,8 @@ testRule('no ambiguous JSX rule', ReactCompilerRule, {
|
||||
return el;
|
||||
}
|
||||
`,
|
||||
errors: [makeTestCaseError('Avoid constructing JSX within try/catch')],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [makeTestCaseError('Avoid constructing JSX within try/catch')],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,15 +4,22 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, makeTestCaseError, testRule} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('no-capitalized-calls', ReactCompilerRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Simple violation',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'no-capitalized-calls',
|
||||
allRules[getRuleForCategory(ErrorCategory.CapitalizedCalls).name].rule,
|
||||
{
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Simple violation',
|
||||
code: normalizeIndent`
|
||||
import Child from './Child';
|
||||
function Component() {
|
||||
return <>
|
||||
@@ -20,13 +27,15 @@ testRule('no-capitalized-calls', ReactCompilerRule, {
|
||||
</>;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Capitalized functions are reserved for components'),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Method call violation',
|
||||
code: normalizeIndent`
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Capitalized functions are reserved for components',
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Method call violation',
|
||||
code: normalizeIndent`
|
||||
import myModule from './MyModule';
|
||||
function Component() {
|
||||
return <>
|
||||
@@ -34,13 +43,15 @@ testRule('no-capitalized-calls', ReactCompilerRule, {
|
||||
</>;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Capitalized functions are reserved for components'),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Multiple diagnostics within the same function are surfaced',
|
||||
code: normalizeIndent`
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Capitalized functions are reserved for components',
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Multiple diagnostics within the same function are surfaced',
|
||||
code: normalizeIndent`
|
||||
import Child1 from './Child1';
|
||||
import MyModule from './MyModule';
|
||||
function Component() {
|
||||
@@ -49,9 +60,12 @@ testRule('no-capitalized-calls', ReactCompilerRule, {
|
||||
{MyModule.Child2()}
|
||||
</>;
|
||||
}`,
|
||||
errors: [
|
||||
makeTestCaseError('Capitalized functions are reserved for components'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Capitalized functions are reserved for components',
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,22 +5,30 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('no ref access in render rule', ReactCompilerRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'validate against simple ref access in render',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'no ref access in render rule',
|
||||
allRules[getRuleForCategory(ErrorCategory.Refs).name].rule,
|
||||
{
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'validate against simple ref access in render',
|
||||
code: normalizeIndent`
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
const value = ref.current;
|
||||
return value;
|
||||
}
|
||||
`,
|
||||
errors: [makeTestCaseError('Cannot access refs during render')],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [makeTestCaseError('Cannot access refs during render')],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,10 +5,19 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
|
||||
import {AllRules} from '../src/rules/ReactCompilerRule';
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {
|
||||
normalizeIndent,
|
||||
testRule,
|
||||
makeTestCaseError,
|
||||
TestRecommendedRules,
|
||||
} from './shared-utils';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('plugin-recommended', AllRules, {
|
||||
testRule('plugin-recommended', TestRecommendedRules, {
|
||||
valid: [
|
||||
{
|
||||
name: 'Basic example with component syntax',
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
*/
|
||||
|
||||
import {RuleTester} from 'eslint';
|
||||
import {CompilerTestCases, normalizeIndent} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {
|
||||
CompilerTestCases,
|
||||
normalizeIndent,
|
||||
TestRecommendedRules,
|
||||
} from './shared-utils';
|
||||
|
||||
const tests: CompilerTestCases = {
|
||||
valid: [
|
||||
@@ -59,4 +62,4 @@ const eslintTester = new RuleTester({
|
||||
// @ts-ignore[2353] - outdated types
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
});
|
||||
eslintTester.run('react-compiler', ReactCompilerRule, tests);
|
||||
eslintTester.run('react-compiler', TestRecommendedRules, tests);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import {RuleTester as ESLintTester, Rule} from 'eslint';
|
||||
import {type ErrorCategory} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import escape from 'regexp.escape';
|
||||
import {configs} from '../src/index';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
/**
|
||||
* A string template tag that removes padding from the left side of multi-line strings
|
||||
@@ -43,4 +46,31 @@ export function testRule(
|
||||
eslintTester.run(name, rule, tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates all recommended rules from the plugin.
|
||||
*/
|
||||
export const TestRecommendedRules: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow capitalized function calls',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
},
|
||||
// validation is done at runtime with zod
|
||||
schema: [{type: 'object', additionalProperties: true}],
|
||||
},
|
||||
create(context) {
|
||||
for (const ruleConfig of Object.values(
|
||||
configs.recommended.plugins['react-compiler'].rules,
|
||||
)) {
|
||||
const listener = ruleConfig.rule.create(context);
|
||||
if (Object.entries(listener).length !== 0) {
|
||||
throw new Error('TODO: handle rules that return listeners to eslint');
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
test('no test', () => {});
|
||||
|
||||
@@ -5,24 +5,37 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import ReactCompilerRule from './rules/ReactCompilerRule';
|
||||
import {type Linter} from 'eslint';
|
||||
import {
|
||||
allRules,
|
||||
mapErrorSeverityToESlint,
|
||||
recommendedRules,
|
||||
} from './rules/ReactCompilerRule';
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
'react-compiler': ReactCompilerRule,
|
||||
},
|
||||
configs: {
|
||||
recommended: {
|
||||
plugins: {
|
||||
'react-compiler': {
|
||||
rules: {
|
||||
'react-compiler': ReactCompilerRule,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react-compiler/react-compiler': 'error',
|
||||
const meta = {
|
||||
name: 'eslint-plugin-react-compiler',
|
||||
};
|
||||
|
||||
const configs = {
|
||||
recommended: {
|
||||
plugins: {
|
||||
'react-compiler': {
|
||||
rules: allRules,
|
||||
},
|
||||
},
|
||||
rules: Object.fromEntries(
|
||||
Object.entries(recommendedRules).map(([name, ruleConfig]) => {
|
||||
return [
|
||||
'react-compiler/' + name,
|
||||
mapErrorSeverityToESlint(ruleConfig.severity),
|
||||
];
|
||||
}),
|
||||
) as Record<string, Linter.StringSeverity>,
|
||||
},
|
||||
};
|
||||
|
||||
const rules = Object.fromEntries(
|
||||
Object.entries(allRules).map(([name, {rule}]) => [name, rule]),
|
||||
);
|
||||
|
||||
export {configs, rules, meta};
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import type {Linter, Rule} from 'eslint';
|
||||
import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler';
|
||||
import {
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
LintRulePreset,
|
||||
LintRules,
|
||||
type LintRule,
|
||||
@@ -108,15 +108,14 @@ function hasFlowSuppression(
|
||||
return false;
|
||||
}
|
||||
|
||||
function makeRule(rules: Array<LintRule>): Rule.RuleModule {
|
||||
const categories = new Set(rules.map(rule => rule.category));
|
||||
function makeRule(rule: LintRule): Rule.RuleModule {
|
||||
const create = (context: Rule.RuleContext): Rule.RuleListener => {
|
||||
const result = getReactCompilerResult(context);
|
||||
|
||||
for (const event of result.events) {
|
||||
if (event.kind === 'CompileError') {
|
||||
const detail = event.detail;
|
||||
if (categories.has(detail.category)) {
|
||||
if (detail.category === rule.category) {
|
||||
const loc = detail.primaryLocation();
|
||||
if (loc == null || typeof loc === 'symbol') {
|
||||
continue;
|
||||
@@ -151,8 +150,8 @@ function makeRule(rules: Array<LintRule>): Rule.RuleModule {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'React Compiler diagnostics',
|
||||
recommended: true,
|
||||
description: rule.description,
|
||||
recommended: rule.preset === LintRulePreset.Recommended,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
@@ -163,13 +162,47 @@ function makeRule(rules: Array<LintRule>): Rule.RuleModule {
|
||||
};
|
||||
}
|
||||
|
||||
export default makeRule(
|
||||
LintRules.filter(
|
||||
rule =>
|
||||
rule.preset === LintRulePreset.Recommended ||
|
||||
rule.preset === LintRulePreset.RecommendedLatest ||
|
||||
rule.category === ErrorCategory.CapitalizedCalls,
|
||||
),
|
||||
);
|
||||
type RulesConfig = {
|
||||
[name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
|
||||
};
|
||||
|
||||
export const AllRules = makeRule(LintRules);
|
||||
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);
|
||||
|
||||
export function mapErrorSeverityToESlint(
|
||||
severity: ErrorSeverity,
|
||||
): Linter.StringSeverity {
|
||||
switch (severity) {
|
||||
case ErrorSeverity.Error: {
|
||||
return 'error';
|
||||
}
|
||||
case ErrorSeverity.Warning: {
|
||||
return 'warn';
|
||||
}
|
||||
case ErrorSeverity.Hint:
|
||||
case ErrorSeverity.Off: {
|
||||
return 'off';
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(severity, `Unhandled severity: ${severity}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,13 +36,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yarn run compile",
|
||||
"build:compiler": "yarn workspace babel-plugin-react-compiler build --dts",
|
||||
"compile": "rimraf dist && concurrently -n server,client \"scripts/build.mjs -t server\" \"scripts/build.mjs -t client\"",
|
||||
"dev": "yarn run package && yarn run install-ext",
|
||||
"install-ext": "code --install-extension react-forgive-0.0.0.vsix",
|
||||
"lint": "echo 'no tests'",
|
||||
"package": "rm -f react-forgive-0.0.0.vsix && vsce package --yarn",
|
||||
"postinstall": "cd client && yarn install && cd ../server && yarn install && cd ..",
|
||||
"pretest": "yarn run compile && yarn run lint",
|
||||
"pretest": "yarn run build:compiler && yarn run compile && yarn run lint",
|
||||
"test": "vscode-test",
|
||||
"vscode:prepublish": "yarn run compile",
|
||||
"watch": "scripts/build.mjs --watch"
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"@babel/parser": "^7.26.0",
|
||||
"@babel/plugin-syntax-typescript": "^7.25.9",
|
||||
"@babel/types": "^7.26.0",
|
||||
"cosmiconfig": "^9.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"vscode-languageserver": "^9.0.1",
|
||||
"vscode-languageserver-textdocument": "^1.0.12"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {SourceLocation} from 'babel-plugin-react-compiler/src';
|
||||
import {type SourceLocation} from 'babel-plugin-react-compiler';
|
||||
import {type Range} from 'vscode-languageserver';
|
||||
|
||||
export function babelLocationToRange(loc: SourceLocation): Range | null {
|
||||
|
||||
@@ -9,7 +9,7 @@ import type * as BabelCore from '@babel/core';
|
||||
import {parseAsync, transformFromAstAsync} from '@babel/core';
|
||||
import BabelPluginReactCompiler, {
|
||||
type PluginOptions,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import * as babelParser from 'prettier/plugins/babel.js';
|
||||
import estreeParser from 'prettier/plugins/estree';
|
||||
import * as typescriptParser from 'prettier/plugins/typescript';
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
parsePluginOptions,
|
||||
type PluginOptions,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import {cosmiconfigSync} from 'cosmiconfig';
|
||||
|
||||
export function resolveReactConfig(projectPath: string): PluginOptions | null {
|
||||
const explorerSync = cosmiconfigSync('react', {
|
||||
searchStrategy: 'project',
|
||||
cache: true,
|
||||
});
|
||||
const result = explorerSync.search(projectPath);
|
||||
if (result != null) {
|
||||
return parsePluginOptions(result.config);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,12 @@ import {
|
||||
TextDocumentSyncKind,
|
||||
} from 'vscode-languageserver/node';
|
||||
import {compile, lastResult} from './compiler';
|
||||
import {type PluginOptions} from 'babel-plugin-react-compiler/src';
|
||||
import {resolveReactConfig} from './compiler/options';
|
||||
import {
|
||||
type CompileSuccessEvent,
|
||||
type LoggerEvent,
|
||||
type PluginOptions,
|
||||
defaultOptions,
|
||||
} from 'babel-plugin-react-compiler/src/Entrypoint/Options';
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat';
|
||||
import {
|
||||
type AutoDepsDecorationsLSPEvent,
|
||||
@@ -64,8 +63,7 @@ type CodeActionLSPEvent = {
|
||||
};
|
||||
|
||||
connection.onInitialize((_params: InitializeParams) => {
|
||||
// TODO(@poteto) get config fr
|
||||
compilerOptions = resolveReactConfig('.') ?? defaultOptions;
|
||||
compilerOptions = defaultOptions;
|
||||
compilerOptions = {
|
||||
...compilerOptions,
|
||||
environment: {
|
||||
@@ -76,21 +74,21 @@ connection.onInitialize((_params: InitializeParams) => {
|
||||
importSpecifierName: 'useEffect',
|
||||
source: 'react',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
autodepsIndex: 1,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
importSpecifierName: 'useSpecialEffect',
|
||||
source: 'shared-runtime',
|
||||
},
|
||||
numRequiredArgs: 2,
|
||||
autodepsIndex: 2,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
importSpecifierName: 'default',
|
||||
source: 'useEffectWrapper',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
autodepsIndex: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler/src/Entrypoint';
|
||||
import {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler';
|
||||
import {type Position} from 'vscode-languageserver-textdocument';
|
||||
import {RequestType} from 'vscode-languageserver/node';
|
||||
import {type Range, sourceLocationToRange} from '../utils/range';
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2":
|
||||
"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2":
|
||||
version "7.26.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
|
||||
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
|
||||
@@ -188,11 +188,6 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
browserslist@^4.24.0:
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2"
|
||||
@@ -203,11 +198,6 @@ browserslist@^4.24.0:
|
||||
node-releases "^2.0.19"
|
||||
update-browserslist-db "^1.1.1"
|
||||
|
||||
callsites@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||
|
||||
caniuse-lite@^1.0.30001688:
|
||||
version "1.0.30001690"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8"
|
||||
@@ -218,16 +208,6 @@ convert-source-map@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
||||
|
||||
cosmiconfig@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d"
|
||||
integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==
|
||||
dependencies:
|
||||
env-paths "^2.2.1"
|
||||
import-fresh "^3.3.0"
|
||||
js-yaml "^4.1.0"
|
||||
parse-json "^5.2.0"
|
||||
|
||||
debug@^4.1.0, debug@^4.3.1:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||
@@ -240,18 +220,6 @@ electron-to-chromium@^1.5.73:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz#cb886b504a6467e4c00bea3317edb38393c53413"
|
||||
integrity sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==
|
||||
|
||||
env-paths@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
|
||||
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
|
||||
|
||||
error-ex@^1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
||||
integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
|
||||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
escalade@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
|
||||
@@ -267,51 +235,21 @@ globals@^11.1.0:
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||
|
||||
import-fresh@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
|
||||
dependencies:
|
||||
parent-module "^1.0.0"
|
||||
resolve-from "^4.0.0"
|
||||
|
||||
is-arrayish@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsesc@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
|
||||
integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
|
||||
|
||||
json-parse-even-better-errors@^2.3.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
|
||||
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
|
||||
|
||||
json5@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||
|
||||
lines-and-columns@^1.1.6:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
lru-cache@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||
@@ -329,23 +267,6 @@ node-releases@^2.0.19:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
|
||||
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
|
||||
dependencies:
|
||||
callsites "^3.0.0"
|
||||
|
||||
parse-json@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
|
||||
integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
error-ex "^1.3.1"
|
||||
json-parse-even-better-errors "^2.3.0"
|
||||
lines-and-columns "^1.1.6"
|
||||
|
||||
picocolors@^1.0.0, picocolors@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
@@ -356,11 +277,6 @@ prettier@^3.3.3:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f"
|
||||
integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
semver@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
## 7.0.1
|
||||
|
||||
- Disallowed passing inline `useEffectEvent` values as JSX props to guard against accidental propagation. ([#34820](https://github.com/facebook/react/pull/34820) by [@jf-eirinha](https://github.com/jf-eirinha))
|
||||
- Switch to `export =` so eslint-plugin-react-hooks emits correct types for consumers in Node16 ESM projects. ([#34949](https://github.com/facebook/react/pull/34949) by [@karlhorky](https://github.com/karlhorky))
|
||||
- Tightened the typing of `configs.flat` so the `configs` export is always defined. ([#34950](https://github.com/facebook/react/pull/34950) by [@poteto](https://github.com/poteto))
|
||||
- Fix named import runtime errors. ([#34951](https://github.com/facebook/react/pull/34951), [#34953](https://github.com/facebook/react/pull/34953) by [@karlhorky](https://github.com/karlhorky))
|
||||
|
||||
## 7.0.0
|
||||
|
||||
This release slims down presets to just 2 configurations (`recommended` and `recommended-latest`), and all compiler rules are enabled by default.
|
||||
|
||||
@@ -5,4 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export {default} from './cjs/eslint-plugin-react-hooks';
|
||||
import reactHooks from './cjs/eslint-plugin-react-hooks';
|
||||
|
||||
export = reactHooks;
|
||||
|
||||
@@ -14,3 +14,13 @@ if (process.env.NODE_ENV === 'production') {
|
||||
} else {
|
||||
module.exports = require('./cjs/eslint-plugin-react-hooks.development.js');
|
||||
}
|
||||
|
||||
// Hint to Node’s cjs-module-lexer to make named imports work
|
||||
// https://github.com/facebook/react/issues/34801#issuecomment-3433478810
|
||||
// eslint-disable-next-line ft-flow/no-unused-expressions
|
||||
0 &&
|
||||
(module.exports = {
|
||||
meta: true,
|
||||
rules: true,
|
||||
configs: true,
|
||||
});
|
||||
|
||||
@@ -71,7 +71,10 @@ const configs = {
|
||||
plugins,
|
||||
rules: recommendedLatestRuleConfigs,
|
||||
},
|
||||
flat: {} as Record<string, ReactHooksFlatConfig>,
|
||||
flat: {} as {
|
||||
recommended: ReactHooksFlatConfig;
|
||||
'recommended-latest': ReactHooksFlatConfig;
|
||||
},
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
|
||||
@@ -64,7 +64,7 @@ function normalizeIOInfo(config: DebugInfoConfig, ioInfo) {
|
||||
if (promise) {
|
||||
promise.then(); // init
|
||||
if (promise.status === 'fulfilled') {
|
||||
if (ioInfo.name === 'RSC stream') {
|
||||
if (ioInfo.name === 'rsc stream') {
|
||||
copy.byteSize = 0;
|
||||
copy.value = {
|
||||
value: 'stream',
|
||||
@@ -117,7 +117,7 @@ export function getDebugInfo(config: DebugInfoConfig, obj) {
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
if (
|
||||
debugInfo[i].awaited &&
|
||||
debugInfo[i].awaited.name === 'RSC stream' &&
|
||||
debugInfo[i].awaited.name === 'rsc stream' &&
|
||||
config.ignoreRscStreamInfo
|
||||
) {
|
||||
// Ignore RSC stream I/O info.
|
||||
|
||||
@@ -2561,6 +2561,7 @@ function ResponseInstance(
|
||||
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugStartTime: void | number, // DEV-only
|
||||
debugChannel: void | DebugChannel, // DEV-only
|
||||
) {
|
||||
const chunks: Map<number, SomeChunk<any>> = new Map();
|
||||
@@ -2621,7 +2622,8 @@ function ResponseInstance(
|
||||
// Note: createFromFetch allows this to be marked at the start of the fetch
|
||||
// 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._debugStartTime =
|
||||
debugStartTime == null ? performance.now() : debugStartTime;
|
||||
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.
|
||||
@@ -2669,6 +2671,7 @@ export function createResponse(
|
||||
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugStartTime: void | number, // DEV-only
|
||||
debugChannel: void | DebugChannel, // DEV-only
|
||||
): WeakResponse {
|
||||
return getWeakResponse(
|
||||
@@ -2684,6 +2687,7 @@ export function createResponse(
|
||||
findSourceMapURL,
|
||||
replayConsole,
|
||||
environmentName,
|
||||
debugStartTime,
|
||||
debugChannel,
|
||||
),
|
||||
);
|
||||
@@ -2717,7 +2721,7 @@ export function createStreamState(
|
||||
(debugValuePromise: any).status = 'fulfilled';
|
||||
(debugValuePromise: any).value = streamDebugValue;
|
||||
streamState._debugInfo = {
|
||||
name: 'RSC stream',
|
||||
name: 'rsc stream',
|
||||
start: response._debugStartTime,
|
||||
end: response._debugStartTime, // will be updated once we finish a chunk
|
||||
byteSize: 0, // will be updated as we resolve a data chunk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-core",
|
||||
"version": "7.0.0",
|
||||
"version": "7.0.1",
|
||||
"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": "7.0.1",
|
||||
"version_name": "7.0.1",
|
||||
"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": "7.0.1",
|
||||
"version_name": "7.0.1",
|
||||
"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": "7.0.1",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "@react-devtools",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
function cloneStyleTags() {
|
||||
const linkTags = [];
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export function cloneStyleTags(): Array<HTMLLinkElement | HTMLStyleElement> {
|
||||
const tags: Array<HTMLLinkElement | HTMLStyleElement> = [];
|
||||
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const linkTag of document.getElementsByTagName('link')) {
|
||||
@@ -11,11 +20,23 @@ function cloneStyleTags() {
|
||||
newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);
|
||||
}
|
||||
|
||||
linkTags.push(newLinkTag);
|
||||
tags.push(newLinkTag);
|
||||
}
|
||||
}
|
||||
|
||||
return linkTags;
|
||||
}
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const styleTag of document.getElementsByTagName('style')) {
|
||||
const newStyleTag = document.createElement('style');
|
||||
|
||||
export default cloneStyleTags;
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const attribute of styleTag.attributes) {
|
||||
newStyleTag.setAttribute(attribute.nodeName, attribute.nodeValue);
|
||||
}
|
||||
|
||||
newStyleTag.textContent = styleTag.textContent;
|
||||
|
||||
tags.push(newStyleTag);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import {viewAttributeSource} from './sourceSelection';
|
||||
|
||||
import {startReactPolling} from './reactPolling';
|
||||
import cloneStyleTags from './cloneStyleTags';
|
||||
import {cloneStyleTags} from './cloneStyleTags';
|
||||
import fetchFileWithCaching from './fetchFileWithCaching';
|
||||
import injectBackendManager from './injectBackendManager';
|
||||
import registerEventsLogger from './registerEventsLogger';
|
||||
|
||||
24
packages/react-devtools-extensions/utils.js
vendored
24
packages/react-devtools-extensions/utils.js
vendored
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
const {execSync} = require('child_process');
|
||||
const {readFileSync} = require('fs');
|
||||
const {existsSync, readFileSync} = require('fs');
|
||||
const {resolve} = require('path');
|
||||
|
||||
const GITHUB_URL = 'https://github.com/facebook/react';
|
||||
@@ -18,8 +18,26 @@ function getGitCommit() {
|
||||
.trim();
|
||||
} catch (error) {
|
||||
// Mozilla runs this command from a git archive.
|
||||
// In that context, there is no Git revision.
|
||||
return null;
|
||||
// In that context, there is no Git context.
|
||||
// Using the commit hash specified to download-experimental-build.js script as a fallback.
|
||||
|
||||
// Try to read from build/COMMIT_SHA file
|
||||
const commitShaPath = resolve(__dirname, '..', '..', 'build', 'COMMIT_SHA');
|
||||
if (!existsSync(commitShaPath)) {
|
||||
throw new Error(
|
||||
'Could not find build/COMMIT_SHA file. Did you run scripts/release/download-experimental-build.js script?',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const commitHash = readFileSync(commitShaPath, 'utf8').trim();
|
||||
// Return short hash (first 7 characters) to match abbreviated commit hash format
|
||||
return commitHash.slice(0, 7);
|
||||
} catch (readError) {
|
||||
throw new Error(
|
||||
`Failed to read build/COMMIT_SHA file: ${readError.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -285,7 +285,7 @@ module.exports = {
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
sourceMap: __DEV__,
|
||||
modules: true,
|
||||
localIdentName: '[local]___[hash:base64:5]',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-inline",
|
||||
"version": "7.0.0",
|
||||
"version": "7.0.1",
|
||||
"description": "Embed react-devtools within a website",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"json5": "^2.2.3",
|
||||
"local-storage-fallback": "^4.1.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.23",
|
||||
"react-window": "^1.8.10"
|
||||
"react-window": "^1.8.10",
|
||||
"rbush": "4.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3243,4 +3243,61 @@ describe('Store', () => {
|
||||
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19.0
|
||||
it('guesses a Suspense name based on the owner', async () => {
|
||||
let resolve;
|
||||
const promise = new Promise(_resolve => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
function Inner() {
|
||||
return (
|
||||
<React.Suspense fallback={<p>Loading inner</p>}>
|
||||
<p>{promise}</p>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function Outer({children}) {
|
||||
return (
|
||||
<React.Suspense fallback={<p>Loading outer</p>}>
|
||||
<p>{promise}</p>
|
||||
{children}
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(() => {
|
||||
render(
|
||||
<Outer>
|
||||
<Inner />
|
||||
</Outer>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Outer>
|
||||
<Suspense>
|
||||
[suspense-root] rects={[{x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="Outer" rects={null}>
|
||||
`);
|
||||
|
||||
console.log('...........................');
|
||||
|
||||
await actAsync(() => {
|
||||
resolve('loaded');
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Outer>
|
||||
▾ <Suspense>
|
||||
▾ <Inner>
|
||||
<Suspense>
|
||||
[suspense-root] rects={[{x:1,y:2,width:6,height:1}, {x:1,y:2,width:6,height:1}]}
|
||||
<Suspense name="Outer" rects={[{x:1,y:2,width:6,height:1}, {x:1,y:2,width:6,height:1}]}>
|
||||
<Suspense name="Inner" rects={[{x:1,y:2,width:6,height:1}]}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1913,6 +1913,20 @@ export function attach(
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUseSyncExternalStoreHook(hookObject: any): boolean {
|
||||
const queue = hookObject.queue;
|
||||
if (!queue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const boundHasOwnProperty = hasOwnProperty.bind(queue);
|
||||
return (
|
||||
boundHasOwnProperty('value') &&
|
||||
boundHasOwnProperty('getSnapshot') &&
|
||||
typeof queue.getSnapshot === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
function isHookThatCanScheduleUpdate(hookObject: any) {
|
||||
const queue = hookObject.queue;
|
||||
if (!queue) {
|
||||
@@ -1929,12 +1943,7 @@ export function attach(
|
||||
return true;
|
||||
}
|
||||
|
||||
// Detect useSyncExternalStore()
|
||||
return (
|
||||
boundHasOwnProperty('value') &&
|
||||
boundHasOwnProperty('getSnapshot') &&
|
||||
typeof queue.getSnapshot === 'function'
|
||||
);
|
||||
return isUseSyncExternalStoreHook(hookObject);
|
||||
}
|
||||
|
||||
function didStatefulHookChange(prev: any, next: any): boolean {
|
||||
@@ -1955,10 +1964,18 @@ export function attach(
|
||||
|
||||
const indices = [];
|
||||
let index = 0;
|
||||
|
||||
while (next !== null) {
|
||||
if (didStatefulHookChange(prev, next)) {
|
||||
indices.push(index);
|
||||
}
|
||||
|
||||
// useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook
|
||||
if (isUseSyncExternalStoreHook(next)) {
|
||||
next = next.next;
|
||||
prev = prev.next;
|
||||
}
|
||||
|
||||
next = next.next;
|
||||
prev = prev.next;
|
||||
index++;
|
||||
@@ -2664,7 +2681,7 @@ export function attach(
|
||||
|
||||
const fiber = fiberInstance.data;
|
||||
const props = fiber.memoizedProps;
|
||||
// TODO: Compute a fallback name based on Owner, key etc.
|
||||
// The frontend will guess a name based on heuristics (e.g. owner) if no explicit name is given.
|
||||
const name =
|
||||
fiber.tag !== SuspenseComponent || props === null
|
||||
? null
|
||||
@@ -2862,7 +2879,10 @@ export function attach(
|
||||
let parentInstance = reconcilingParent;
|
||||
while (
|
||||
parentInstance.kind === FILTERED_FIBER_INSTANCE &&
|
||||
parentInstance.parent !== null
|
||||
parentInstance.parent !== null &&
|
||||
// We can't move past the parent Suspense node.
|
||||
// The Suspense node holding async info must be a parent of the devtools instance (or the instance itself)
|
||||
parentInstance !== parentSuspenseNode.instance
|
||||
) {
|
||||
parentInstance = parentInstance.parent;
|
||||
}
|
||||
@@ -6168,7 +6188,10 @@ export function attach(
|
||||
}
|
||||
}
|
||||
const newIO = asyncInfo.awaited;
|
||||
if (newIO.name === 'RSC stream' && newIO.value != null) {
|
||||
if (
|
||||
(newIO.name === 'RSC stream' || newIO.name === 'rsc stream') &&
|
||||
newIO.value != null
|
||||
) {
|
||||
const streamPromise = newIO.value;
|
||||
// Special case RSC stream entries to pick the last entry keyed by the stream.
|
||||
const existingEntry = streamEntries.get(streamPromise);
|
||||
@@ -6227,7 +6250,10 @@ export function attach(
|
||||
continue;
|
||||
}
|
||||
foundIOEntries.add(ioInfo);
|
||||
if (ioInfo.name === 'RSC stream' && ioInfo.value != null) {
|
||||
if (
|
||||
(ioInfo.name === 'RSC stream' || ioInfo.name === 'rsc stream') &&
|
||||
ioInfo.value != null
|
||||
) {
|
||||
const streamPromise = ioInfo.value;
|
||||
// Special case RSC stream entries to pick the last entry keyed by the stream.
|
||||
const existingEntry = streamEntries.get(streamPromise);
|
||||
|
||||
@@ -62,6 +62,31 @@ import type {
|
||||
import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError';
|
||||
import type {DevToolsHookSettings} from '../backend/types';
|
||||
|
||||
import RBush from 'rbush';
|
||||
|
||||
// Custom version which works with our Rect data structure.
|
||||
class RectRBush extends RBush<Rect> {
|
||||
toBBox(rect: Rect): {
|
||||
minX: number,
|
||||
minY: number,
|
||||
maxX: number,
|
||||
maxY: number,
|
||||
} {
|
||||
return {
|
||||
minX: rect.x,
|
||||
minY: rect.y,
|
||||
maxX: rect.x + rect.width,
|
||||
maxY: rect.y + rect.height,
|
||||
};
|
||||
}
|
||||
compareMinX(a: Rect, b: Rect): number {
|
||||
return a.x - b.x;
|
||||
}
|
||||
compareMinY(a: Rect, b: Rect): number {
|
||||
return a.y - b.y;
|
||||
}
|
||||
}
|
||||
|
||||
const debug = (methodName: string, ...args: Array<string>) => {
|
||||
if (__DEBUG__) {
|
||||
console.log(
|
||||
@@ -194,6 +219,9 @@ export default class Store extends EventEmitter<{
|
||||
// Renderer ID is needed to support inspection fiber props, state, and hooks.
|
||||
_rootIDToRendererID: Map<Element['id'], number> = new Map();
|
||||
|
||||
// Stores all the SuspenseNode rects in an R-tree to make it fast to find overlaps.
|
||||
_rtree: RBush<Rect> = new RectRBush();
|
||||
|
||||
// These options may be initially set by a configuration option when constructing the Store.
|
||||
_supportsInspectMatchingDOMElement: boolean = false;
|
||||
_supportsClickToInspect: boolean = false;
|
||||
@@ -1622,7 +1650,12 @@ export default class Store extends EventEmitter<{
|
||||
const y = operations[i + 1] / 1000;
|
||||
const width = operations[i + 2] / 1000;
|
||||
const height = operations[i + 3] / 1000;
|
||||
rects.push({x, y, width, height});
|
||||
const rect = {x, y, width, height};
|
||||
if (parentID !== 0) {
|
||||
// Track all rects except the root.
|
||||
this._rtree.insert(rect);
|
||||
}
|
||||
rects.push(rect);
|
||||
i += 4;
|
||||
}
|
||||
}
|
||||
@@ -1646,10 +1679,6 @@ export default class Store extends EventEmitter<{
|
||||
parentSuspense.children.push(id);
|
||||
}
|
||||
|
||||
if (name === null) {
|
||||
name = 'Unknown';
|
||||
}
|
||||
|
||||
this._idToSuspense.set(id, {
|
||||
id,
|
||||
parentID,
|
||||
@@ -1684,13 +1713,20 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
i += 1;
|
||||
|
||||
const {children, parentID} = suspense;
|
||||
const {children, parentID, rects} = suspense;
|
||||
if (children.length > 0) {
|
||||
this._throwAndEmitError(
|
||||
Error(`Suspense node "${id}" was removed before its children.`),
|
||||
);
|
||||
}
|
||||
|
||||
if (rects !== null && parentID !== 0) {
|
||||
// Delete all the existing rects from the R-tree
|
||||
for (let j = 0; j < rects.length; j++) {
|
||||
this._rtree.remove(rects[j]);
|
||||
}
|
||||
}
|
||||
|
||||
this._idToSuspense.delete(id);
|
||||
removedSuspenseIDs.set(id, parentID);
|
||||
|
||||
@@ -1789,6 +1825,14 @@ export default class Store extends EventEmitter<{
|
||||
break;
|
||||
}
|
||||
|
||||
const prevRects = suspense.rects;
|
||||
if (prevRects !== null && suspense.parentID !== 0) {
|
||||
// Delete all the existing rects from the R-tree
|
||||
for (let j = 0; j < prevRects.length; j++) {
|
||||
this._rtree.remove(prevRects[j]);
|
||||
}
|
||||
}
|
||||
|
||||
let nextRects: SuspenseNode['rects'];
|
||||
if (numRects === -1) {
|
||||
nextRects = null;
|
||||
@@ -1800,7 +1844,12 @@ export default class Store extends EventEmitter<{
|
||||
const width = operations[i + 2] / 1000;
|
||||
const height = operations[i + 3] / 1000;
|
||||
|
||||
nextRects.push({x, y, width, height});
|
||||
const rect = {x, y, width, height};
|
||||
if (suspense.parentID !== 0) {
|
||||
// Track all rects except the root.
|
||||
this._rtree.insert(rect);
|
||||
}
|
||||
nextRects.push(rect);
|
||||
|
||||
i += 4;
|
||||
}
|
||||
@@ -2170,13 +2219,12 @@ export default class Store extends EventEmitter<{
|
||||
throw error;
|
||||
}
|
||||
|
||||
_guessSuspenseName(element: Element): string {
|
||||
_guessSuspenseName(element: Element): string | null {
|
||||
const owner = this._idToElement.get(element.ownerID);
|
||||
let name = 'Unknown';
|
||||
if (owner !== undefined && owner.displayName !== null) {
|
||||
name = owner.displayName;
|
||||
return owner.displayName;
|
||||
}
|
||||
|
||||
return name;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +63,7 @@ function printRects(rects: SuspenseNode['rects']): string {
|
||||
}
|
||||
|
||||
function printSuspense(suspense: SuspenseNode): string {
|
||||
let name = '';
|
||||
if (suspense.name !== null) {
|
||||
name = ` name="${suspense.name}"`;
|
||||
}
|
||||
|
||||
const name = ` name="${suspense.name || 'Unknown'}"`;
|
||||
const printedRects = printRects(suspense.rects);
|
||||
|
||||
return `<Suspense${name}${printedRects}>`;
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.CallSite {
|
||||
display: block;
|
||||
.CallSite, .BuiltInCallSite {
|
||||
display: flex;
|
||||
padding-left: 1rem;
|
||||
white-space-collapse: preserve;
|
||||
}
|
||||
|
||||
.IgnoredCallSite, .BuiltInCallSite {
|
||||
@@ -20,13 +21,15 @@
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
border-radius: 0.125rem;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
|
||||
.Link:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.ElementBadges {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,10 @@ export function CallSiteView({
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ElementBadges environmentName={environmentName} />
|
||||
<ElementBadges
|
||||
className={styles.ElementBadges}
|
||||
environmentName={environmentName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function SuspenseBreadcrumbs(): React$Node {
|
||||
className={styles.SuspenseBreadcrumbsButton}
|
||||
onClick={handleClick.bind(null, id)}
|
||||
type="button">
|
||||
{node === null ? 'Unknown' : node.name}
|
||||
{node === null ? 'Unknown' : node.name || 'Unknown'}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,25 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.SuspenseRectsTitle {
|
||||
pointer-events: none;
|
||||
color: color-mix(in srgb, var(--color-suspense) 50%, var(--color-text));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-sans-small);
|
||||
line-height: var(--line-height-data);
|
||||
padding: 0 .25rem;
|
||||
container-type: size;
|
||||
container-name: title;
|
||||
}
|
||||
|
||||
@container title (width < 30px) or (height < 18px) {
|
||||
.SuspenseRectsTitle > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.SuspenseRectsScaledRect[data-visible='false'] > .SuspenseRectsBoundaryChildren {
|
||||
overflow: initial;
|
||||
}
|
||||
@@ -75,7 +94,7 @@
|
||||
transition: background-color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.SuspenseRectsBoundary[data-selected='true'] {
|
||||
.SuspenseRectsBoundary[data-selected='true'][data-visible='true'] {
|
||||
box-shadow: var(--elevation-4);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user