diff --git a/.gitignore b/.gitignore index 2a20fc2427..12efc9468c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ chrome-user-data .vscode *.swp *.swo +*.vsix packages/react-devtools-core/dist packages/react-devtools-extensions/chrome/build @@ -37,3 +38,4 @@ packages/react-devtools-fusebox/dist packages/react-devtools-inline/dist packages/react-devtools-shell/dist packages/react-devtools-timeline/dist + diff --git a/compiler/package.json b/compiler/package.json index c05e0e70d3..b25031b996 100644 --- a/compiler/package.json +++ b/compiler/package.json @@ -26,11 +26,11 @@ "react-is": "0.0.0-experimental-4beb1fd8-20241118" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-terser": "^0.4.4", - "@rollup/plugin-typescript": "^11.1.6", + "@rollup/plugin-typescript": "^12.1.2", "@tsconfig/strictest": "^2.0.5", "concurrently": "^7.4.0", "folder-hash": "^4.0.4", diff --git a/compiler/packages/babel-plugin-react-compiler/package.json b/compiler/packages/babel-plugin-react-compiler/package.json index 158b800dba..428f70926d 100644 --- a/compiler/packages/babel-plugin-react-compiler/package.json +++ b/compiler/packages/babel-plugin-react-compiler/package.json @@ -42,6 +42,7 @@ "babel-jest": "^29.0.3", "babel-plugin-fbt": "^1.0.0", "babel-plugin-fbt-runtime": "^1.0.0", + "cosmiconfig": "^9.0.0", "eslint": "^8.57.1", "invariant": "^2.2.4", "jest": "^29.0.3", diff --git a/compiler/packages/babel-plugin-react-compiler/rollup.config.js b/compiler/packages/babel-plugin-react-compiler/rollup.config.js index 77e785c464..bccc69f423 100644 --- a/compiler/packages/babel-plugin-react-compiler/rollup.config.js +++ b/compiler/packages/babel-plugin-react-compiler/rollup.config.js @@ -15,7 +15,7 @@ import terser from '@rollup/plugin-terser'; import prettier from 'rollup-plugin-prettier'; import banner2 from 'rollup-plugin-banner2'; -const NO_INLINE = new Set(['@babel/types']); +const NO_INLINE = new Set([]); const DEV_ROLLUP_CONFIG = { input: 'src/index.ts', @@ -24,6 +24,7 @@ const DEV_ROLLUP_CONFIG = { format: 'cjs', sourcemap: false, exports: 'named', + inlineDynamicImports: true, }, plugins: [ typescript({ diff --git a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts index c648c66043..72d254d995 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts @@ -6,7 +6,12 @@ */ import type * as BabelCore from '@babel/core'; -import {compileProgram, parsePluginOptions} from '../Entrypoint'; +import { + compileProgram, + findReactConfig, + parsePluginOptions, + type PluginOptions, +} from '../Entrypoint'; import { injectReanimatedFlag, pipelineUsesReanimatedPlugin, @@ -29,7 +34,18 @@ export default function BabelPluginReactCompiler( * want Forget to run true to source as possible. */ Program(prog, pass): void { - let opts = parsePluginOptions(pass.opts); + const reactConfig = findReactConfig(); + let opts: PluginOptions | null = null; + if (reactConfig != null) { + opts = parsePluginOptions(reactConfig.config); + if (pass.opts != null) { + console.warn( + `Duplicate React Compiler config found, defaulting to reactrc found in: ${reactConfig.filepath}`, + ); + } + } else { + opts = parsePluginOptions(pass.opts); + } const isDev = (typeof __DEV__ !== 'undefined' && __DEV__ === true) || process.env['NODE_ENV'] === 'development'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index fb951d25c5..9d0ff3fce9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -16,6 +16,7 @@ import { import {hasOwnProperty} from '../Utils/utils'; import {fromZodError} from 'zod-validation-error'; import {CompilerPipelineValue} from './Pipeline'; +import {type CosmiconfigResult, cosmiconfigSync} from 'cosmiconfig'; const PanicThresholdOptionsSchema = z.enum([ /* @@ -286,3 +287,11 @@ export function parseTargetConfig(value: unknown): CompilerReactTarget { function isCompilerFlag(s: string): s is keyof PluginOptions { return hasOwnProperty(defaultOptions, s); } + +export function findReactConfig(): CosmiconfigResult { + const explorerSync = cosmiconfigSync('react', { + searchStrategy: 'project', + cache: true, + }); + return explorerSync.search(); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index c48cba32b2..ca6abc0748 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -98,6 +98,7 @@ import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryState import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR'; import {outlineJSX} from '../Optimization/OutlineJsx'; import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls'; +import {transformFire} from '../Transform'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -197,6 +198,11 @@ function runWithEnvironment( validateHooksUsage(hir); } + if (env.config.enableFire) { + transformFire(hir); + log({kind: 'hir', name: 'TransformFire', value: hir}); + } + if (env.config.validateNoCapitalizedCalls) { validateNoCapitalizedCalls(hir); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index a6e09a1d06..bb0d662c4f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -564,6 +564,14 @@ export function compileProgram( if (environment.enableChangeDetectionForDebugging != null) { externalFunctions.push(environment.enableChangeDetectionForDebugging); } + + const hasFireRewrite = compiledFns.some(c => c.compiledFn.hasFireRewrite); + if (environment.enableFire && hasFireRewrite) { + externalFunctions.push({ + source: getReactCompilerRuntimeModule(pass.opts), + importSpecifierName: 'useFire', + }); + } } catch (err) { handleError(err, pass, null); return; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index e2932296ca..f3f426df56 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -787,6 +787,7 @@ export class Environment { fnType: ReactFunctionType; useMemoCacheIdentifier: string; hasLoweredContextAccess: boolean; + hasFireRewrite: boolean; #contextIdentifiers: Set; #hoistedIdentifiers: Set; @@ -811,6 +812,7 @@ export class Environment { this.#shapes = new Map(DEFAULT_SHAPES); this.#globals = new Map(DEFAULT_GLOBALS); this.hasLoweredContextAccess = false; + this.hasFireRewrite = false; if ( config.disableMemoizationForDebugging && diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index 526ab7c7e5..a6f6c606e1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -897,6 +897,14 @@ export function printSourceLocation(loc: SourceLocation): string { } } +export function printSourceLocationLine(loc: SourceLocation): string { + if (typeof loc === 'symbol') { + return 'generated'; + } else { + return `${loc.start.line}:${loc.end.line}`; + } +} + export function printAliases(aliases: DisjointSet): string { const aliasSets = aliases.buildSets(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index b2f1b9e6d4..b9ec688d87 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -103,6 +103,11 @@ export type CodegenFunction = { * This is true if the compiler has the lowered useContext calls. */ hasLoweredContextAccess: boolean; + + /** + * This is true if the compiler has compiled a fire to a useFire call + */ + hasFireRewrite: boolean; }; export function codegenFunction( @@ -355,6 +360,7 @@ function codegenReactiveFunction( prunedMemoValues: countMemoBlockVisitor.prunedMemoValues, outlined: [], hasLoweredContextAccess: fn.env.hasLoweredContextAccess, + hasFireRewrite: fn.env.hasFireRewrite, }); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts new file mode 100644 index 0000000000..a35c4ddb01 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -0,0 +1,760 @@ +/** + * 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 { + CompilerError, + CompilerErrorDetailOptions, + ErrorSeverity, + SourceLocation, +} from '..'; +import { + ArrayExpression, + CallExpression, + Effect, + Environment, + FunctionExpression, + GeneratedSource, + HIRFunction, + Identifier, + IdentifierId, + Instruction, + InstructionId, + InstructionKind, + InstructionValue, + isUseEffectHookType, + LoadLocal, + makeInstructionId, + Place, + promoteTemporary, +} from '../HIR'; +import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder'; +import {getOrInsertWith} from '../Utils/utils'; +import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape'; +import {eachInstructionOperand} from '../HIR/visitors'; +import {printSourceLocationLine} from '../HIR/PrintHIR'; + +/* + * TODO(jmbrown): + * - traverse object methods + * - method calls + * - React.useEffect calls + */ + +const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`'; + +export function transformFire(fn: HIRFunction): void { + const context = new Context(fn.env); + replaceFireFunctions(fn, context); + if (!context.hasErrors()) { + ensureNoMoreFireUses(fn, context); + } + context.throwIfErrorsFound(); +} + +function replaceFireFunctions(fn: HIRFunction, context: Context): void { + let hasRewrite = false; + for (const [, block] of fn.body.blocks) { + const rewriteInstrs = new Map>(); + const deleteInstrs = new Set(); + for (const instr of block.instructions) { + const {value, lvalue} = instr; + if ( + value.kind === 'CallExpression' && + isUseEffectHookType(value.callee.identifier) && + value.args.length > 0 && + value.args[0].kind === 'Identifier' + ) { + const lambda = context.getFunctionExpression( + value.args[0].identifier.id, + ); + if (lambda != null) { + const capturedCallees = + visitFunctionExpressionAndPropagateFireDependencies( + lambda, + context, + true, + ); + + // Add useFire calls for all fire calls in found in the lambda + const newInstrs = []; + for (const [ + fireCalleePlace, + fireCalleeInfo, + ] of capturedCallees.entries()) { + if (!context.hasCalleeWithInsertedFire(fireCalleePlace)) { + context.addCalleeWithInsertedFire(fireCalleePlace); + const loadUseFireInstr = makeLoadUseFireInstruction(fn.env); + const loadFireCalleeInstr = makeLoadFireCalleeInstruction( + fn.env, + fireCalleeInfo.capturedCalleeIdentifier, + ); + const callUseFireInstr = makeCallUseFireInstruction( + fn.env, + loadUseFireInstr.lvalue, + loadFireCalleeInstr.lvalue, + ); + const storeUseFireInstr = makeStoreUseFireInstruction( + fn.env, + callUseFireInstr.lvalue, + fireCalleeInfo.fireFunctionBinding, + ); + newInstrs.push( + loadUseFireInstr, + loadFireCalleeInstr, + callUseFireInstr, + storeUseFireInstr, + ); + + // We insert all of these instructions before the useEffect is loaded + const loadUseEffectInstrId = context.getLoadGlobalInstrId( + value.callee.identifier.id, + ); + if (loadUseEffectInstrId == null) { + context.pushError({ + loc: value.loc, + description: null, + severity: ErrorSeverity.Invariant, + reason: '[InsertFire] No LoadGlobal found for useEffect call', + suggestions: null, + }); + continue; + } + rewriteInstrs.set(loadUseEffectInstrId, newInstrs); + } + } + ensureNoRemainingCalleeCaptures( + lambda.loweredFunc.func, + context, + capturedCallees, + ); + + if ( + value.args.length > 1 && + value.args[1] != null && + value.args[1].kind === 'Identifier' + ) { + const depArray = value.args[1]; + const depArrayExpression = context.getArrayExpression( + depArray.identifier.id, + ); + if (depArrayExpression != null) { + for (const dependency of depArrayExpression.elements) { + if (dependency.kind === 'Identifier') { + const loadOfDependency = context.getLoadLocalInstr( + dependency.identifier.id, + ); + if (loadOfDependency != null) { + const replacedDepArrayItem = capturedCallees.get( + loadOfDependency.place.identifier.id, + ); + if (replacedDepArrayItem != null) { + loadOfDependency.place = + replacedDepArrayItem.fireFunctionBinding; + } + } + } + } + } else { + context.pushError({ + loc: value.args[1].loc, + description: + 'You must use an array literal for an effect dependency array when that effect uses `fire()`', + severity: ErrorSeverity.Invariant, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } else if (value.args.length > 1 && value.args[1].kind === 'Spread') { + context.pushError({ + loc: value.args[1].place.loc, + description: + 'You must use an array literal for an effect dependency array when that effect uses `fire()`', + severity: ErrorSeverity.Invariant, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } + } else if ( + value.kind === 'CallExpression' && + value.callee.identifier.type.kind === 'Function' && + value.callee.identifier.type.shapeId === BuiltInFireId && + context.inUseEffectLambda() + ) { + /* + * We found a fire(callExpr()) call. We remove the `fire()` call and replace the callExpr() + * with a freshly generated fire function binding. We'll insert the useFire call before the + * useEffect call, which happens in the CallExpression (useEffect) case above. + */ + + /* + * We only allow fire to be called with a CallExpression: `fire(f())` + * TODO: add support for method calls: `fire(this.method())` + */ + if (value.args.length === 1 && value.args[0].kind === 'Identifier') { + const callExpr = context.getCallExpression( + value.args[0].identifier.id, + ); + + if (callExpr != null) { + const calleeId = callExpr.callee.identifier.id; + const loadLocal = context.getLoadLocalInstr(calleeId); + if (loadLocal == null) { + context.pushError({ + loc: value.loc, + description: null, + severity: ErrorSeverity.Invariant, + reason: + '[InsertFire] No loadLocal found for fire call argument', + suggestions: null, + }); + continue; + } + + const fireFunctionBinding = + context.getOrGenerateFireFunctionBinding( + loadLocal.place, + value.loc, + ); + + loadLocal.place = {...fireFunctionBinding}; + + // Delete the fire call expression + deleteInstrs.add(instr.id); + } else { + context.pushError({ + loc: value.loc, + description: + '`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed', + severity: ErrorSeverity.InvalidReact, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } else { + let description: string = + 'fire() can only take in a single call expression as an argument'; + if (value.args.length === 0) { + description += ' but received none'; + } else if (value.args.length > 1) { + description += ' but received multiple arguments'; + } else if (value.args[0].kind === 'Spread') { + description += ' but received a spread argument'; + } + context.pushError({ + loc: value.loc, + description, + severity: ErrorSeverity.InvalidReact, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } else if (value.kind === 'CallExpression') { + context.addCallExpression(lvalue.identifier.id, value); + } else if ( + value.kind === 'FunctionExpression' && + context.inUseEffectLambda() + ) { + visitFunctionExpressionAndPropagateFireDependencies( + value, + context, + false, + ); + } else if (value.kind === 'FunctionExpression') { + context.addFunctionExpression(lvalue.identifier.id, value); + } else if (value.kind === 'LoadLocal') { + context.addLoadLocalInstr(lvalue.identifier.id, value); + } else if ( + value.kind === 'LoadGlobal' && + value.binding.kind === 'ImportSpecifier' && + value.binding.module === 'react' && + value.binding.imported === 'fire' && + context.inUseEffectLambda() + ) { + deleteInstrs.add(instr.id); + } else if (value.kind === 'LoadGlobal') { + context.addLoadGlobalInstrId(lvalue.identifier.id, instr.id); + } else if (value.kind === 'ArrayExpression') { + context.addArrayExpression(lvalue.identifier.id, value); + } + } + block.instructions = rewriteInstructions(rewriteInstrs, block.instructions); + block.instructions = deleteInstructions(deleteInstrs, block.instructions); + + if (rewriteInstrs.size > 0 || deleteInstrs.size > 0) { + hasRewrite = true; + fn.env.hasFireRewrite = true; + } + } + + if (hasRewrite) { + markInstructionIds(fn.body); + } +} + +/** + * Traverses a function expression to find fire calls fire(foo()) and replaces them with + * fireFoo(). + * + * When a function captures a fire call we need to update its context to reflect the newly created + * fire function bindings and update the LoadLocals referenced by the function's dependencies. + * + * @param isUseEffect is necessary so we can keep track of when we should additionally insert + * useFire hooks calls. + */ +function visitFunctionExpressionAndPropagateFireDependencies( + fnExpr: FunctionExpression, + context: Context, + enteringUseEffect: boolean, +): FireCalleesToFireFunctionBinding { + let withScope = enteringUseEffect + ? context.withUseEffectLambdaScope.bind(context) + : context.withFunctionScope.bind(context); + + const calleesCapturedByFnExpression = withScope(() => + replaceFireFunctions(fnExpr.loweredFunc.func, context), + ); + + /* + * Make a mapping from each dependency to the corresponding LoadLocal for it so that + * we can replace the loaded place with the generated fire function binding + */ + const loadLocalsToDepLoads = new Map(); + for (const dep of fnExpr.loweredFunc.dependencies) { + const loadLocal = context.getLoadLocalInstr(dep.identifier.id); + if (loadLocal != null) { + loadLocalsToDepLoads.set(loadLocal.place.identifier.id, loadLocal); + } + } + + const replacedCallees = new Map(); + for (const [ + calleeIdentifierId, + loadedFireFunctionBindingPlace, + ] of calleesCapturedByFnExpression.entries()) { + /* + * Given the ids of captured fire callees, look at the deps for loads of those identifiers + * and replace them with the new fire function binding + */ + const loadLocal = loadLocalsToDepLoads.get(calleeIdentifierId); + if (loadLocal == null) { + context.pushError({ + loc: fnExpr.loc, + description: null, + severity: ErrorSeverity.Invariant, + reason: + '[InsertFire] No loadLocal found for fire call argument for lambda', + suggestions: null, + }); + continue; + } + + const oldPlaceId = loadLocal.place.identifier.id; + loadLocal.place = { + ...loadedFireFunctionBindingPlace.fireFunctionBinding, + }; + + replacedCallees.set( + oldPlaceId, + loadedFireFunctionBindingPlace.fireFunctionBinding, + ); + } + + // For each replaced callee, update the context of the function expression to track it + for ( + let contextIdx = 0; + contextIdx < fnExpr.loweredFunc.func.context.length; + contextIdx++ + ) { + const contextItem = fnExpr.loweredFunc.func.context[contextIdx]; + const replacedCallee = replacedCallees.get(contextItem.identifier.id); + if (replacedCallee != null) { + fnExpr.loweredFunc.func.context[contextIdx] = replacedCallee; + } + } + + context.mergeCalleesFromInnerScope(calleesCapturedByFnExpression); + + return calleesCapturedByFnExpression; +} + +/* + * eachInstructionOperand is not sufficient for our cases because: + * 1. fire is a global, which will not appear + * 2. The HIR may be malformed, so can't rely on function deps and must + * traverse the whole function. + */ +function* eachReachablePlace(fn: HIRFunction): Iterable { + for (const [, block] of fn.body.blocks) { + for (const instr of block.instructions) { + if ( + instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod' + ) { + yield* eachReachablePlace(instr.value.loweredFunc.func); + } else { + yield* eachInstructionOperand(instr); + } + } + } +} + +function ensureNoRemainingCalleeCaptures( + fn: HIRFunction, + context: Context, + capturedCallees: FireCalleesToFireFunctionBinding, +): void { + for (const place of eachReachablePlace(fn)) { + const calleeInfo = capturedCallees.get(place.identifier.id); + if (calleeInfo != null) { + const calleeName = + calleeInfo.capturedCalleeIdentifier.name?.kind === 'named' + ? calleeInfo.capturedCalleeIdentifier.name.value + : ''; + context.pushError({ + loc: place.loc, + description: `All uses of ${calleeName} must be either used with a fire() call in \ +this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \ +${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`, + severity: ErrorSeverity.InvalidReact, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } +} + +function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void { + for (const place of eachReachablePlace(fn)) { + if ( + place.identifier.type.kind === 'Function' && + place.identifier.type.shapeId === BuiltInFireId + ) { + context.pushError({ + loc: place.identifier.loc, + description: 'Cannot use `fire` outside of a useEffect function', + severity: ErrorSeverity.Invariant, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } +} + +function makeLoadUseFireInstruction(env: Environment): Instruction { + const useFirePlace = createTemporaryPlace(env, GeneratedSource); + useFirePlace.effect = Effect.Read; + useFirePlace.identifier.type = DefaultNonmutatingHook; + const instrValue: InstructionValue = { + kind: 'LoadGlobal', + binding: { + kind: 'ImportSpecifier', + name: 'useFire', + module: 'react', + imported: 'useFire', + }, + loc: GeneratedSource, + }; + return { + id: makeInstructionId(0), + value: instrValue, + lvalue: {...useFirePlace}, + loc: GeneratedSource, + }; +} + +function makeLoadFireCalleeInstruction( + env: Environment, + fireCalleeIdentifier: Identifier, +): Instruction { + const loadedFireCallee = createTemporaryPlace(env, GeneratedSource); + const fireCallee: Place = { + kind: 'Identifier', + identifier: fireCalleeIdentifier, + reactive: false, + effect: Effect.Unknown, + loc: fireCalleeIdentifier.loc, + }; + return { + id: makeInstructionId(0), + value: { + kind: 'LoadLocal', + loc: GeneratedSource, + place: {...fireCallee}, + }, + lvalue: {...loadedFireCallee}, + loc: GeneratedSource, + }; +} + +function makeCallUseFireInstruction( + env: Environment, + useFirePlace: Place, + argPlace: Place, +): Instruction { + const useFireCallResultPlace = createTemporaryPlace(env, GeneratedSource); + useFireCallResultPlace.effect = Effect.Read; + + const useFireCall: CallExpression = { + kind: 'CallExpression', + callee: {...useFirePlace}, + args: [argPlace], + loc: GeneratedSource, + }; + + return { + id: makeInstructionId(0), + value: useFireCall, + lvalue: {...useFireCallResultPlace}, + loc: GeneratedSource, + }; +} + +function makeStoreUseFireInstruction( + env: Environment, + useFireCallResultPlace: Place, + fireFunctionBindingPlace: Place, +): Instruction { + promoteTemporary(fireFunctionBindingPlace.identifier); + + const fireFunctionBindingLValuePlace = createTemporaryPlace( + env, + GeneratedSource, + ); + return { + id: makeInstructionId(0), + value: { + kind: 'StoreLocal', + lvalue: { + kind: InstructionKind.Const, + place: {...fireFunctionBindingPlace}, + }, + value: {...useFireCallResultPlace}, + type: null, + loc: GeneratedSource, + }, + lvalue: fireFunctionBindingLValuePlace, + loc: GeneratedSource, + }; +} + +type FireCalleesToFireFunctionBinding = Map< + IdentifierId, + { + fireFunctionBinding: Place; + capturedCalleeIdentifier: Identifier; + fireLoc: SourceLocation; + } +>; + +class Context { + #env: Environment; + + #errors: CompilerError = new CompilerError(); + + /* + * Used to look up the call expression passed to a `fire(callExpr())`. Gives back + * the `callExpr()`. + */ + #callExpressions = new Map(); + + /* + * We keep track of function expressions so that we can traverse them when + * we encounter a lambda passed to a useEffect call + */ + #functionExpressions = new Map(); + + /* + * Mapping from lvalue ids to the LoadLocal for it. Allows us to replace dependency LoadLocals. + */ + #loadLocals = new Map(); + + /* + * Maps all of the fire callees found in a component/hook to the generated fire function places + * we create for them. Allows us to reuse already-inserted useFire results + */ + #fireCalleesToFireFunctions: Map = new Map(); + + /* + * The callees for which we have already created fire bindings. Used to skip inserting a new + * useFire call for a fire callee if one has already been created. + */ + #calleesWithInsertedFire = new Set(); + + /* + * A mapping from fire callees to the created fire function bindings that are reachable from this + * scope. + * + * We additionally keep track of the captured callee identifier so that we can properly reference + * it in the place where we LoadLocal the callee as an argument to useFire. + */ + #capturedCalleeIdentifierIds: FireCalleesToFireFunctionBinding = new Map(); + + /* + * We only transform fire calls if we're syntactically within a useEffect lambda (for now) + */ + #inUseEffectLambda = false; + + /* + * Mapping from useEffect callee identifier ids to the instruction id of the + * load global instruction for the useEffect call. We use this to insert the + * useFire calls before the useEffect call + */ + #loadGlobalInstructionIds = new Map(); + + constructor(env: Environment) { + this.#env = env; + } + + /* + * We keep track of array expressions so we can rewrite dependency arrays passed to useEffect + * to use the fire functions + */ + #arrayExpressions = new Map(); + + pushError(error: CompilerErrorDetailOptions): void { + this.#errors.push(error); + } + + withFunctionScope(fn: () => void): FireCalleesToFireFunctionBinding { + fn(); + return this.#capturedCalleeIdentifierIds; + } + + withUseEffectLambdaScope(fn: () => void): FireCalleesToFireFunctionBinding { + const capturedCalleeIdentifierIds = this.#capturedCalleeIdentifierIds; + const inUseEffectLambda = this.#inUseEffectLambda; + + this.#capturedCalleeIdentifierIds = new Map(); + this.#inUseEffectLambda = true; + + const resultCapturedCalleeIdentifierIds = this.withFunctionScope(fn); + + this.#capturedCalleeIdentifierIds = capturedCalleeIdentifierIds; + this.#inUseEffectLambda = inUseEffectLambda; + + return resultCapturedCalleeIdentifierIds; + } + + addCallExpression(id: IdentifierId, callExpr: CallExpression): void { + this.#callExpressions.set(id, callExpr); + } + + getCallExpression(id: IdentifierId): CallExpression | undefined { + return this.#callExpressions.get(id); + } + + addLoadLocalInstr(id: IdentifierId, loadLocal: LoadLocal): void { + this.#loadLocals.set(id, loadLocal); + } + + getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined { + return this.#loadLocals.get(id); + } + getOrGenerateFireFunctionBinding( + callee: Place, + fireLoc: SourceLocation, + ): Place { + const fireFunctionBinding = getOrInsertWith( + this.#fireCalleesToFireFunctions, + callee.identifier.id, + () => createTemporaryPlace(this.#env, GeneratedSource), + ); + + this.#capturedCalleeIdentifierIds.set(callee.identifier.id, { + fireFunctionBinding, + capturedCalleeIdentifier: callee.identifier, + fireLoc, + }); + + return fireFunctionBinding; + } + + mergeCalleesFromInnerScope( + innerCallees: FireCalleesToFireFunctionBinding, + ): void { + for (const [id, calleeInfo] of innerCallees.entries()) { + this.#capturedCalleeIdentifierIds.set(id, calleeInfo); + } + } + + addCalleeWithInsertedFire(id: IdentifierId): void { + this.#calleesWithInsertedFire.add(id); + } + + hasCalleeWithInsertedFire(id: IdentifierId): boolean { + return this.#calleesWithInsertedFire.has(id); + } + + inUseEffectLambda(): boolean { + return this.#inUseEffectLambda; + } + + addFunctionExpression(id: IdentifierId, fn: FunctionExpression): void { + this.#functionExpressions.set(id, fn); + } + + getFunctionExpression(id: IdentifierId): FunctionExpression | undefined { + return this.#functionExpressions.get(id); + } + + addLoadGlobalInstrId(id: IdentifierId, instrId: InstructionId): void { + this.#loadGlobalInstructionIds.set(id, instrId); + } + + getLoadGlobalInstrId(id: IdentifierId): InstructionId | undefined { + return this.#loadGlobalInstructionIds.get(id); + } + + addArrayExpression(id: IdentifierId, array: ArrayExpression): void { + this.#arrayExpressions.set(id, array); + } + + getArrayExpression(id: IdentifierId): ArrayExpression | undefined { + return this.#arrayExpressions.get(id); + } + + hasErrors(): boolean { + return this.#errors.hasErrors(); + } + + throwIfErrorsFound(): void { + if (this.hasErrors()) throw this.#errors; + } +} + +function deleteInstructions( + deleteInstrs: Set, + instructions: Array, +): Array { + if (deleteInstrs.size > 0) { + const newInstrs = instructions.filter(instr => !deleteInstrs.has(instr.id)); + return newInstrs; + } + return instructions; +} + +function rewriteInstructions( + rewriteInstrs: Map>, + instructions: Array, +): Array { + if (rewriteInstrs.size > 0) { + const newInstrs = []; + for (const instr of instructions) { + const newInstrsAtId = rewriteInstrs.get(instr.id); + if (newInstrsAtId != null) { + newInstrs.push(...newInstrsAtId, instr); + } else { + newInstrs.push(instr); + } + } + + return newInstrs; + } + + return instructions; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts new file mode 100644 index 0000000000..8665ead0b1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts @@ -0,0 +1,7 @@ +/** + * 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. + */ +export {transformFire} from './TransformFire'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index b361b2016a..4db8c700f3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -305,6 +305,14 @@ function validateNoRefAccessInRenderImpl( ); break; } + case 'TypeCastExpression': { + env.set( + instr.lvalue.identifier.id, + env.get(instr.value.value.identifier.id) ?? + refTypeOfType(instr.lvalue), + ); + break; + } case 'LoadContext': case 'LoadLocal': { env.set( diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md new file mode 100644 index 0000000000..56e3039f63 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +import {useRef} from 'react'; + +function useArrayOfRef() { + const ref = useRef(null); + const callback = value => { + ref.current = value; + }; + return [callback] as const; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => { + useArrayOfRef(); + return 'ok'; + }, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef } from "react"; + +function useArrayOfRef() { + const $ = _c(1); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const callback = (value) => { + ref.current = value; + }; + + t0 = [callback]; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0 as const; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => { + useArrayOfRef(); + return "ok"; + }, + + params: [{}], +}; + +``` + +### Eval output +(kind: ok) "ok" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js new file mode 100644 index 0000000000..2d0aafeffd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js @@ -0,0 +1,17 @@ +import {useRef} from 'react'; + +function useArrayOfRef() { + const ref = useRef(null); + const callback = value => { + ref.current = value; + }; + return [callback] as const; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => { + useArrayOfRef(); + return 'ok'; + }, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md new file mode 100644 index 0000000000..8d8bc179a2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { useFire } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(3); + const foo = _temp; + const t0 = useFire(foo); + let t1; + if ($[0] !== props || $[1] !== t0) { + t1 = () => { + t0(props); + }; + $[0] = props; + $[1] = t0; + $[2] = t1; + } else { + t1 = $[2]; + } + useEffect(t1); + return null; +} +function _temp(props_0) { + console.log(props_0); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.js new file mode 100644 index 0000000000..2f7a72e4ee --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md new file mode 100644 index 0000000000..a335fea886 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md @@ -0,0 +1,74 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + function nested() { + function nestedAgain() { + function nestedThrice() { + fire(foo(props)); + } + nestedThrice(); + } + nestedAgain(); + } + nested(); + }); + + return null; +} + +``` + +## Code + +```javascript +import { useFire } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(3); + const foo = _temp; + const t0 = useFire(foo); + let t1; + if ($[0] !== props || $[1] !== t0) { + t1 = () => { + const nested = function nested() { + const nestedAgain = function nestedAgain() { + const nestedThrice = function nestedThrice() { + t0(props); + }; + + nestedThrice(); + }; + + nestedAgain(); + }; + + nested(); + }; + $[0] = props; + $[1] = t0; + $[2] = t1; + } else { + t1 = $[2]; + } + useEffect(t1); + return null; +} +function _temp(props_0) { + console.log(props_0); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.js new file mode 100644 index 0000000000..b056c3f53a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.js @@ -0,0 +1,22 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + function nested() { + function nestedAgain() { + function nestedThrice() { + fire(foo(props)); + } + nestedThrice(); + } + nestedAgain(); + } + nested(); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.expect.md new file mode 100644 index 0000000000..a24f27a695 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @enableFire +import {fire, useEffect} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + if (props.cond) { + useEffect(() => { + fire(foo(props)); + }); + } + + return null; +} + +``` + + +## Error + +``` + 8 | + 9 | if (props.cond) { +> 10 | useEffect(() => { + | ^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (10:10) + 11 | fire(foo(props)); + 12 | }); + 13 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.js new file mode 100644 index 0000000000..30ae8e59b9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.js @@ -0,0 +1,16 @@ +// @enableFire +import {fire, useEffect} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + if (props.cond) { + useEffect(() => { + fire(foo(props)); + }); + } + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md new file mode 100644 index 0000000000..e73451a896 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + function nested() { + fire(foo(props)); + foo(props); + } + + nested(); + }); + + return null; +} + +``` + + +## Error + +``` + 9 | function nested() { + 10 | fire(foo(props)); +> 11 | foo(props); + | ^^^ InvalidReact: Cannot compile `fire`. All uses of foo must be either used with a fire() call in this effect or not used with a fire() call at all. foo was used with fire() on line 10:10 in this effect (11:11) + 12 | } + 13 | + 14 | nested(); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.js new file mode 100644 index 0000000000..ee2f915a34 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.js @@ -0,0 +1,18 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + function nested() { + fire(foo(props)); + foo(props); + } + + nested(); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md new file mode 100644 index 0000000000..8329717cb3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar, baz); + }; + useEffect(() => { + fire(foo(bar), baz); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | fire(foo(bar), baz); + | ^^^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received multiple arguments (9:9) + 10 | }); + 11 | + 12 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.js new file mode 100644 index 0000000000..980b0dfcb5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar, baz); + }; + useEffect(() => { + fire(foo(bar), baz); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md new file mode 100644 index 0000000000..580fd6a2a6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @enable +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + useEffect(() => { + function nested() { + fire(foo(props)); + } + + nested(); + }); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | useEffect(() => { + | ^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function component (9:9) + 10 | function nested() { + 11 | fire(foo(props)); + 12 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.js new file mode 100644 index 0000000000..16f2425724 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.js @@ -0,0 +1,19 @@ +// @enable +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + useEffect(() => { + function nested() { + fire(foo(props)); + } + + nested(); + }); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md new file mode 100644 index 0000000000..855c7b7d70 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(props); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | fire(props); + | ^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (9:9) + 10 | }); + 11 | + 12 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.js new file mode 100644 index 0000000000..3d1ae3658f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(props); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md new file mode 100644 index 0000000000..687a21f98c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md @@ -0,0 +1,38 @@ + +## Input + +```javascript +// @enableFire +import {fire, useCallback} from 'react'; + +function Component({props, bar}) { + const foo = () => { + console.log(props); + }; + fire(foo(props)); + + useCallback(() => { + fire(foo(props)); + }, [foo, props]); + + return null; +} + +``` + + +## Error + +``` + 6 | console.log(props); + 7 | }; +> 8 | fire(foo(props)); + | ^^^^ Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (8:8) + +Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (11:11) + 9 | + 10 | useCallback(() => { + 11 | fire(foo(props)); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.js new file mode 100644 index 0000000000..8ac9be6d76 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.js @@ -0,0 +1,15 @@ +// @enableFire +import {fire, useCallback} from 'react'; + +function Component({props, bar}) { + const foo = () => { + console.log(props); + }; + fire(foo(props)); + + useCallback(() => { + fire(foo(props)); + }, [foo, props]); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md new file mode 100644 index 0000000000..dcd9312bb2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + const deps = [foo, props]; + + useEffect(() => { + fire(foo(props)); + }, deps); + + return null; +} + +``` + + +## Error + +``` + 11 | useEffect(() => { + 12 | fire(foo(props)); +> 13 | }, deps); + | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (13:13) + 14 | + 15 | return null; + 16 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.js new file mode 100644 index 0000000000..b82f735425 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.js @@ -0,0 +1,16 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + const deps = [foo, props]; + + useEffect(() => { + fire(foo(props)); + }, deps); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md new file mode 100644 index 0000000000..7c1b55f61d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + const deps = [foo, props]; + + useEffect(() => { + fire(foo(props)); + }, ...deps); + + return null; +} + +``` + + +## Error + +``` + 11 | useEffect(() => { + 12 | fire(foo(props)); +> 13 | }, ...deps); + | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (13:13) + 14 | + 15 | return null; + 16 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.js new file mode 100644 index 0000000000..27d1de4f46 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.js @@ -0,0 +1,19 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + const deps = [foo, props]; + + useEffect( + () => { + fire(foo(props)); + }, + ...deps + ); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md new file mode 100644 index 0000000000..c0b797fc14 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(...foo); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | fire(...foo); + | ^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received a spread argument (9:9) + 10 | }); + 11 | + 12 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.js new file mode 100644 index 0000000000..68e317588b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(...foo); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md new file mode 100644 index 0000000000..3f237cfc6f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(props.foo()); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | fire(props.foo()); + | ^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (9:9) + 10 | }); + 11 | + 12 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.js new file mode 100644 index 0000000000..c75622ca5e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(props.foo()); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md new file mode 100644 index 0000000000..5767ff0746 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +// @enableFire @inferEffectDependencies +import {fire, useEffect} from 'react'; + +function Component(props) { + const foo = arg => { + console.log(arg, props.bar); + }; + useEffect(() => { + fire(foo(props)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { useFire } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableFire @inferEffectDependencies +import { fire, useEffect } from "react"; + +function Component(props) { + const $ = _c(5); + let t0; + if ($[0] !== props.bar) { + t0 = (arg) => { + console.log(arg, props.bar); + }; + $[0] = props.bar; + $[1] = t0; + } else { + t0 = $[1]; + } + const foo = t0; + const t1 = useFire(foo); + let t2; + if ($[2] !== props || $[3] !== t1) { + t2 = () => { + t1(props); + }; + $[2] = props; + $[3] = t1; + $[4] = t2; + } else { + t2 = $[4]; + } + useEffect(t2, [t1, props]); + return null; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js new file mode 100644 index 0000000000..e2a0068a19 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js @@ -0,0 +1,13 @@ +// @enableFire @inferEffectDependencies +import {fire, useEffect} from 'react'; + +function Component(props) { + const foo = arg => { + console.log(arg, props.bar); + }; + useEffect(() => { + fire(foo(props)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md new file mode 100644 index 0000000000..02f3935171 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + function nested() { + fire(foo(props)); + function innerNested() { + fire(foo(props)); + } + } + + nested(); + }); + + return null; +} + +``` + +## Code + +```javascript +import { useFire } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(3); + const foo = _temp; + const t0 = useFire(foo); + let t1; + if ($[0] !== props || $[1] !== t0) { + t1 = () => { + t0(props); + const nested = function nested() { + t0(props); + }; + + nested(); + }; + $[0] = props; + $[1] = t0; + $[2] = t1; + } else { + t1 = $[2]; + } + useEffect(t1); + return null; +} +function _temp(props_0) { + console.log(props_0); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.js new file mode 100644 index 0000000000..54410680e6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.js @@ -0,0 +1,21 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + function nested() { + fire(foo(props)); + function innerNested() { + fire(foo(props)); + } + } + + nested(); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md new file mode 100644 index 0000000000..1734ca3ab4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md @@ -0,0 +1,62 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + fire(foo(props)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { useFire } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(5); + let t0; + if ($[0] !== props) { + t0 = () => { + console.log(props); + }; + $[0] = props; + $[1] = t0; + } else { + t0 = $[1]; + } + const foo = t0; + const t1 = useFire(foo); + let t2; + if ($[2] !== props || $[3] !== t1) { + t2 = () => { + t1(props); + t1(props); + }; + $[2] = props; + $[3] = t1; + $[4] = t2; + } else { + t2 = $[4]; + } + useEffect(t2); + return null; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.js new file mode 100644 index 0000000000..14e1cb06b1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.js @@ -0,0 +1,14 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + fire(foo(props)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.expect.md new file mode 100644 index 0000000000..ae71f60393 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + }, [foo, props]); + + return null; +} + +``` + +## Code + +```javascript +import { useFire } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(4); + const foo = _temp; + const t0 = useFire(foo); + let t1; + let t2; + if ($[0] !== props || $[1] !== t0) { + t1 = () => { + t0(props); + }; + t2 = [t0, props]; + $[0] = props; + $[1] = t0; + $[2] = t1; + $[3] = t2; + } else { + t1 = $[2]; + t2 = $[3]; + } + useEffect(t1, t2); + return null; +} +function _temp(props_0) { + console.log(props_0); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.js new file mode 100644 index 0000000000..ad1af704c1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + }, [foo, props]); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md new file mode 100644 index 0000000000..9b689b31c7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md @@ -0,0 +1,81 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar); + }; + useEffect(() => { + fire(foo(bar)); + fire(baz(bar)); + }); + + useEffect(() => { + fire(foo(bar)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { useFire } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(t0) { + const $ = _c(9); + const { bar, baz } = t0; + let t1; + if ($[0] !== bar) { + t1 = () => { + console.log(bar); + }; + $[0] = bar; + $[1] = t1; + } else { + t1 = $[1]; + } + const foo = t1; + const t2 = useFire(foo); + const t3 = useFire(baz); + let t4; + if ($[2] !== bar || $[3] !== t2 || $[4] !== t3) { + t4 = () => { + t2(bar); + t3(bar); + }; + $[2] = bar; + $[3] = t2; + $[4] = t3; + $[5] = t4; + } else { + t4 = $[5]; + } + useEffect(t4); + let t5; + if ($[6] !== bar || $[7] !== t2) { + t5 = () => { + t2(bar); + }; + $[6] = bar; + $[7] = t2; + $[8] = t5; + } else { + t5 = $[8]; + } + useEffect(t5); + return null; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.js new file mode 100644 index 0000000000..5cb51e9bd3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.js @@ -0,0 +1,18 @@ +// @enableFire +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar); + }; + useEffect(() => { + fire(foo(bar)); + fire(baz(bar)); + }); + + useEffect(() => { + fire(foo(bar)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.expect.md new file mode 100644 index 0000000000..f482ac44dd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.expect.md @@ -0,0 +1,30 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + useEffect(); + + return null; +} + +``` + +## Code + +```javascript +// @enableFire +import { fire } from "react"; + +function Component(props) { + useEffect(); + return null; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.js new file mode 100644 index 0000000000..731c45df67 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.js @@ -0,0 +1,8 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + useEffect(); + + return null; +} diff --git a/compiler/packages/eslint-plugin-react-compiler/rollup.config.js b/compiler/packages/eslint-plugin-react-compiler/rollup.config.js index 4d81356409..743e4cc844 100644 --- a/compiler/packages/eslint-plugin-react-compiler/rollup.config.js +++ b/compiler/packages/eslint-plugin-react-compiler/rollup.config.js @@ -29,6 +29,7 @@ const DEV_ROLLUP_CONFIG = { file: 'dist/index.js', format: 'cjs', sourcemap: false, + inlineDynamicImports: true, }, treeshake: { moduleSideEffects: false, diff --git a/compiler/packages/react-compiler-healthcheck/rollup.config.js b/compiler/packages/react-compiler-healthcheck/rollup.config.js index 117974ad6b..0c2492d140 100644 --- a/compiler/packages/react-compiler-healthcheck/rollup.config.js +++ b/compiler/packages/react-compiler-healthcheck/rollup.config.js @@ -33,6 +33,7 @@ const DEV_ROLLUP_CONFIG = { format: 'cjs', sourcemap: false, exports: 'named', + inlineDynamicImports: true, }, plugins: [ typescript({ diff --git a/compiler/packages/react-compiler-runtime/package.json b/compiler/packages/react-compiler-runtime/package.json index 575ec847a5..d72f168c56 100644 --- a/compiler/packages/react-compiler-runtime/package.json +++ b/compiler/packages/react-compiler-runtime/package.json @@ -9,7 +9,7 @@ "src" ], "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" }, "scripts": { "build": "rimraf dist && rollup --config --bundleConfigAsCjs", diff --git a/compiler/packages/react-compiler-runtime/rollup.config.js b/compiler/packages/react-compiler-runtime/rollup.config.js index 260359dd0c..2399f2160c 100644 --- a/compiler/packages/react-compiler-runtime/rollup.config.js +++ b/compiler/packages/react-compiler-runtime/rollup.config.js @@ -21,6 +21,7 @@ const PROD_ROLLUP_CONFIG = { file: 'dist/index.js', format: 'cjs', sourcemap: true, + inlineDynamicImports: true, }, plugins: [ typescript({ diff --git a/compiler/packages/react-forgive/.vscodeignore b/compiler/packages/react-forgive/.vscodeignore new file mode 100644 index 0000000000..420c614521 --- /dev/null +++ b/compiler/packages/react-forgive/.vscodeignore @@ -0,0 +1,3 @@ +**/node_modules +client +server diff --git a/compiler/packages/react-forgive/.yarnrc b/compiler/packages/react-forgive/.yarnrc new file mode 100644 index 0000000000..123ac74a0a --- /dev/null +++ b/compiler/packages/react-forgive/.yarnrc @@ -0,0 +1 @@ +ignore-engines true diff --git a/compiler/packages/react-forgive/LICENSE b/compiler/packages/react-forgive/LICENSE new file mode 100644 index 0000000000..b93be90515 --- /dev/null +++ b/compiler/packages/react-forgive/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Meta Platforms, Inc. and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/compiler/packages/react-forgive/client/package.json b/compiler/packages/react-forgive/client/package.json new file mode 100644 index 0000000000..a975439726 --- /dev/null +++ b/compiler/packages/react-forgive/client/package.json @@ -0,0 +1,22 @@ +{ + "private": "true", + "name": "react-forgive-client", + "version": "0.0.0", + "description": "Experimental LSP client", + "license": "MIT", + "scripts": { + "build": "echo 'no build'", + "test": "echo 'no tests'" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/facebook/react.git", + "directory": "compiler/packages/react-forgive" + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/vscode": "^1.95.0" + } +} diff --git a/compiler/packages/react-forgive/client/src/extension.ts b/compiler/packages/react-forgive/client/src/extension.ts new file mode 100644 index 0000000000..cb5abae5ed --- /dev/null +++ b/compiler/packages/react-forgive/client/src/extension.ts @@ -0,0 +1,62 @@ +import * as path from 'path'; +import {ExtensionContext, window as Window} from 'vscode'; + +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, + TransportKind, +} from 'vscode-languageclient/node'; + +let client: LanguageClient; + +export function activate(context: ExtensionContext) { + const serverModule = context.asAbsolutePath(path.join('dist', 'server.js')); + + // If the extension is launched in debug mode then the debug server options are used + // Otherwise the run options are used + const serverOptions: ServerOptions = { + run: { + module: serverModule, + transport: TransportKind.ipc, + options: {cwd: process.cwd()}, + }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + options: {cwd: process.cwd()}, + }, + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [ + {scheme: 'file', language: 'javascriptreact'}, + {scheme: 'file', language: 'typescriptreact'}, + ], + progressOnInitialization: true, + }; + + // Create the language client and start the client. + try { + client = new LanguageClient( + 'react-forgive', + 'React Analyzer', + serverOptions, + clientOptions, + ); + } catch { + Window.showErrorMessage( + `React Analyzer couldn't be started. See the output channel for details.`, + ); + return; + } + + client.registerProposedFeatures(); + client.start(); +} + +export function deactivate(): Thenable | undefined { + if (client !== undefined) { + return client.stop(); + } +} diff --git a/compiler/packages/react-forgive/client/yarn.lock b/compiler/packages/react-forgive/client/yarn.lock new file mode 100644 index 0000000000..b96751788c --- /dev/null +++ b/compiler/packages/react-forgive/client/yarn.lock @@ -0,0 +1,59 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/vscode@^1.95.0": + version "1.96.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.96.0.tgz#3181004bf25d71677ae4aacdd7605a3fd7edf08e" + integrity sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +minimatch@^5.1.0: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +semver@^7.3.7: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== + +vscode-languageclient@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz#cdfe20267726c8d4db839dc1e9d1816e1296e854" + integrity sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA== + dependencies: + minimatch "^5.1.0" + semver "^7.3.7" + vscode-languageserver-protocol "3.17.5" + +vscode-languageserver-protocol@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== + dependencies: + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== diff --git a/compiler/packages/react-forgive/package.json b/compiler/packages/react-forgive/package.json new file mode 100644 index 0000000000..1c6e941b9d --- /dev/null +++ b/compiler/packages/react-forgive/package.json @@ -0,0 +1,58 @@ +{ + "name": "react-forgive", + "displayName": "React Analyzer", + "description": "React LSP", + "license": "MIT", + "version": "0.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/facebook/react.git", + "directory": "compiler/packages/react-forgive" + }, + "categories": [ + "Programming Languages" + ], + "keywords": [ + "react", + "react analyzer", + "react compiler" + ], + "publisher": "Meta", + "engines": { + "vscode": "^1.75.0" + }, + "activationEvents": [ + "onLanguage:javascriptreact", + "onLanguage:typescriptreact" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "react-forgive.toggleAll", + "title": "React Analyzer: Toggle on/off" + } + ] + }, + "scripts": { + "compile": "rimraf dist && concurrently -n server,client \"yarn run esbuild:server --sourcemap\" \"yarn run esbuild:client --sourcemap\"", + "dev": "yarn run package && yarn run install-ext", + "esbuild:client": "esbuild ./client/src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node", + "esbuild:server": "esbuild ./server/src/index.ts --bundle --outfile=dist/server.js --external:vscode --format=cjs --platform=node", + "install-ext": "code --install-extension react-forgive-0.0.0.vsix", + "lint": "eslint src --ext ts", + "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", + "test": "vscode-test", + "vscode:prepublish": "yarn run compile", + "watch": "concurrently --kill-others -n server,client \"run esbuild:server --sourcemap --watch\" \"run esbuild:client --sourcemap --watch\"" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/node": "^20", + "esbuild": "^0.24.0", + "eslint": "^9.13.0", + "typescript-eslint": "^8.16.0" + } +} diff --git a/compiler/packages/react-forgive/server/package.json b/compiler/packages/react-forgive/server/package.json new file mode 100644 index 0000000000..5e120f0271 --- /dev/null +++ b/compiler/packages/react-forgive/server/package.json @@ -0,0 +1,27 @@ +{ + "private": "true", + "name": "react-forgive-server", + "version": "0.0.0", + "description": "Experimental LSP server", + "license": "MIT", + "main": "dist/index.js", + "scripts": { + "build": "rimraf dist && rollup --config --bundleConfigAsCjs", + "test": "echo 'no tests'" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/facebook/react.git", + "directory": "compiler/packages/react-forgive" + }, + "dependencies": { + "@babel/core": "^7.26.0", + "@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" + } +} diff --git a/compiler/packages/react-forgive/server/src/compiler/index.ts b/compiler/packages/react-forgive/server/src/compiler/index.ts new file mode 100644 index 0000000000..8ce5386183 --- /dev/null +++ b/compiler/packages/react-forgive/server/src/compiler/index.ts @@ -0,0 +1,58 @@ +/** + * 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 type * as BabelCore from '@babel/core'; +import {parseAsync, transformFromAstAsync} from '@babel/core'; +import BabelPluginReactCompiler, { + type PluginOptions, +} from 'babel-plugin-react-compiler/src'; +import * as babelParser from 'prettier/plugins/babel.js'; +import estreeParser from 'prettier/plugins/estree'; +import * as typescriptParser from 'prettier/plugins/typescript'; +import * as prettier from 'prettier/standalone'; + +type CompileOptions = { + text: string; + file: string; + options: PluginOptions | null; +}; +export async function compile({ + text, + file, + options, +}: CompileOptions): Promise { + const ast = await parseAsync(text, { + sourceFileName: file, + parserOpts: { + plugins: ['typescript', 'jsx'], + }, + sourceType: 'module', + }); + const plugins = + options != null + ? [[BabelPluginReactCompiler, options]] + : [[BabelPluginReactCompiler]]; + const result = await transformFromAstAsync(ast, text, { + filename: file, + highlightCode: false, + retainLines: true, + plugins, + sourceType: 'module', + sourceFileName: file, + }); + if (result?.code == null) { + throw new Error( + `Expected BabelPluginReactCompiler to compile successfully, got ${result}`, + ); + } + result.code = await prettier.format(result.code, { + semi: false, + parser: 'babel-ts', + plugins: [babelParser, estreeParser, typescriptParser], + }); + return result; +} diff --git a/compiler/packages/react-forgive/server/src/compiler/options.ts b/compiler/packages/react-forgive/server/src/compiler/options.ts new file mode 100644 index 0000000000..226be799d3 --- /dev/null +++ b/compiler/packages/react-forgive/server/src/compiler/options.ts @@ -0,0 +1,25 @@ +/** + * 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; + } +} diff --git a/compiler/packages/react-forgive/server/src/index.ts b/compiler/packages/react-forgive/server/src/index.ts new file mode 100644 index 0000000000..b86a07292e --- /dev/null +++ b/compiler/packages/react-forgive/server/src/index.ts @@ -0,0 +1,93 @@ +/** + * 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 {TextDocument} from 'vscode-languageserver-textdocument'; +import { + createConnection, + type InitializeParams, + type InitializeResult, + ProposedFeatures, + TextDocuments, + TextDocumentSyncKind, +} from 'vscode-languageserver/node'; +import {compile} from './compiler'; +import {type PluginOptions} from 'babel-plugin-react-compiler/src'; +import {resolveReactConfig} from './compiler/options'; + +const SUPPORTED_LANGUAGE_IDS = new Set([ + 'javascript', + 'javascriptreact', + 'typescript', + 'typescriptreact', +]); + +const connection = createConnection(ProposedFeatures.all); +connection.console.info(`React Analyzer running in node ${process.version}`); + +const compiledCache = new WeakMap(); + +const documents = new TextDocuments(TextDocument); +documents.listen(connection); + +let compilerOptions: PluginOptions | null = null; + +connection.onInitialize((_params: InitializeParams) => { + // TODO(@poteto) get config fr + compilerOptions = resolveReactConfig('.'); + const result: InitializeResult = { + capabilities: { + textDocumentSync: TextDocumentSyncKind.Full, + codeLensProvider: {resolveProvider: true}, + }, + }; + return result; +}); + +connection.onInitialized(() => { + connection.console.log('initialized'); +}); + +documents.onDidOpen(async event => { + if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) { + const result = await compile({ + text: event.document.getText(), + file: event.document.uri, + options: compilerOptions, + }); + if (result.code != null) { + compiledCache.set(event.document, result.code); + connection.console.log(result.code); + } + } +}); + +documents.onDidChangeContent(async event => { + if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) { + const result = await compile({ + text: event.document.getText(), + file: event.document.uri, + options: compilerOptions, + }); + if (result.code != null) { + compiledCache.set(event.document, result.code); + connection.console.log(result.code); + } + } +}); + +connection.onDidChangeWatchedFiles(change => { + connection.console.log( + change.changes.map(c => `File changed: ${c.uri}`).join('\n'), + ); +}); + +connection.onCodeLens(params => { + connection.console.log(JSON.stringify(params, null, 2)); + return []; +}); + +connection.listen(); diff --git a/compiler/packages/react-forgive/server/tsconfig.json b/compiler/packages/react-forgive/server/tsconfig.json new file mode 100644 index 0000000000..88422f5c91 --- /dev/null +++ b/compiler/packages/react-forgive/server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@tsconfig/strictest/tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "moduleResolution": "Bundler", + "rootDir": "../../..", + "noEmit": true, + "jsx": "react-jsxdev", + "target": "ES2015", + "sourceMap": false, + "removeComments": true, + + "strictNullChecks": false + }, + "exclude": ["node_modules", ".vscode-test"], + "include": ["src/**/*.ts"] +} diff --git a/compiler/packages/react-forgive/server/yarn.lock b/compiler/packages/react-forgive/server/yarn.lock new file mode 100644 index 0000000000..b72063294f --- /dev/null +++ b/compiler/packages/react-forgive/server/yarn.lock @@ -0,0 +1,410 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@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": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" + integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== + +"@babel/core@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" + integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.26.0" + "@babel/generator" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.0" + "@babel/parser" "^7.26.0" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.26.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.26.0", "@babel/generator@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== + dependencies: + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" + integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== + dependencies: + "@babel/compat-data" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-plugin-utils@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" + integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helpers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" + integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== + dependencies: + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.0" + +"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== + dependencies: + "@babel/types" "^7.26.3" + +"@babel/plugin-syntax-typescript@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" + integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/traverse@^7.25.9": + version "7.26.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.4.tgz#ac3a2a84b908dde6d463c3bfa2c5fdc1653574bd" + integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.3" + "@babel/parser" "^7.26.3" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.3" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@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" + integrity sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + 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" + integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w== + +convert-source-map@^2.0.0: + version "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" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +electron-to-chromium@^1.5.73: + version "1.5.74" + 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" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +globals@^11.1.0: + version "11.12.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" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +node-releases@^2.0.19: + version "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" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +prettier@^3.3.3: + version "3.4.2" + 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" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +update-browserslist-db@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== + +vscode-languageserver-protocol@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== + dependencies: + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-textdocument@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz#457ee04271ab38998a093c68c2342f53f6e4a631" + integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA== + +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-languageserver@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz#500aef82097eb94df90d008678b0b6b5f474015b" + integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g== + dependencies: + vscode-languageserver-protocol "3.17.5" + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/compiler/yarn.lock b/compiler/yarn.lock index b4c72ff3c5..d31672dbfd 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -1700,6 +1700,126 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@esbuild/aix-ppc64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c" + integrity sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw== + +"@esbuild/android-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz#1add7e0af67acefd556e407f8497e81fddad79c0" + integrity sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w== + +"@esbuild/android-arm@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.0.tgz#ab7263045fa8e090833a8e3c393b60d59a789810" + integrity sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew== + +"@esbuild/android-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.0.tgz#e8f8b196cfdfdd5aeaebbdb0110983460440e705" + integrity sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ== + +"@esbuild/darwin-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz#2d0d9414f2acbffd2d86e98253914fca603a53dd" + integrity sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw== + +"@esbuild/darwin-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz#33087aab31a1eb64c89daf3d2cf8ce1775656107" + integrity sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA== + +"@esbuild/freebsd-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz#bb76e5ea9e97fa3c753472f19421075d3a33e8a7" + integrity sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA== + +"@esbuild/freebsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz#e0e2ce9249fdf6ee29e5dc3d420c7007fa579b93" + integrity sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ== + +"@esbuild/linux-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz#d1b2aa58085f73ecf45533c07c82d81235388e75" + integrity sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g== + +"@esbuild/linux-arm@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz#8e4915df8ea3e12b690a057e77a47b1d5935ef6d" + integrity sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw== + +"@esbuild/linux-ia32@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz#8200b1110666c39ab316572324b7af63d82013fb" + integrity sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA== + +"@esbuild/linux-loong64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz#6ff0c99cf647504df321d0640f0d32e557da745c" + integrity sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g== + +"@esbuild/linux-mips64el@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz#3f720ccd4d59bfeb4c2ce276a46b77ad380fa1f3" + integrity sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA== + +"@esbuild/linux-ppc64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz#9d6b188b15c25afd2e213474bf5f31e42e3aa09e" + integrity sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ== + +"@esbuild/linux-riscv64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz#f989fdc9752dfda286c9cd87c46248e4dfecbc25" + integrity sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw== + +"@esbuild/linux-s390x@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz#29ebf87e4132ea659c1489fce63cd8509d1c7319" + integrity sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g== + +"@esbuild/linux-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz#4af48c5c0479569b1f359ffbce22d15f261c0cef" + integrity sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA== + +"@esbuild/netbsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz#1ae73d23cc044a0ebd4f198334416fb26c31366c" + integrity sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg== + +"@esbuild/openbsd-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz#5d904a4f5158c89859fd902c427f96d6a9e632e2" + integrity sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg== + +"@esbuild/openbsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz#4c8aa88c49187c601bae2971e71c6dc5e0ad1cdf" + integrity sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q== + +"@esbuild/sunos-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz#8ddc35a0ea38575fa44eda30a5ee01ae2fa54dd4" + integrity sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA== + +"@esbuild/win32-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz#6e79c8543f282c4539db684a207ae0e174a9007b" + integrity sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA== + +"@esbuild/win32-ia32@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz#057af345da256b7192d18b676a02e95d0fa39103" + integrity sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw== + +"@esbuild/win32-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244" + integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1712,11 +1832,32 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== +"@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + "@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": version "4.10.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== +"@eslint/config-array@^0.19.0": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.1.tgz#734aaea2c40be22bbb1f2a9dac687c57a6a4c984" + integrity sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA== + dependencies: + "@eslint/object-schema" "^2.1.5" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/core@^0.9.0": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.9.1.tgz#31763847308ef6b7084a4505573ac9402c51f9d1" + integrity sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q== + dependencies: + "@types/json-schema" "^7.0.15" + "@eslint/eslintrc@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" @@ -1732,6 +1873,21 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/eslintrc@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c" + integrity sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + "@eslint/js@8.57.0": version "8.57.0" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" @@ -1742,6 +1898,23 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@eslint/js@9.17.0", "@eslint/js@^9.13.0": + version "9.17.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.17.0.tgz#1523e586791f80376a6f8398a3964455ecc651ec" + integrity sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w== + +"@eslint/object-schema@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.5.tgz#8670a8f6258a2be5b2c620ff314a1d984c23eb2e" + integrity sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ== + +"@eslint/plugin-kit@^0.2.3": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz#2b78e7bb3755784bb13faa8932a1d994d6537792" + integrity sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg== + dependencies: + levn "^0.4.1" + "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -1754,6 +1927,19 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -1787,6 +1973,16 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b" + integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -2512,17 +2708,18 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@rollup/plugin-commonjs@^25.0.7": - version "25.0.7" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf" - integrity sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ== +"@rollup/plugin-commonjs@^28.0.2": + version "28.0.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz#193d7a86470f112b56927c1d821ee45951a819ea" + integrity sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw== dependencies: "@rollup/pluginutils" "^5.0.1" commondir "^1.0.1" estree-walker "^2.0.2" - glob "^8.0.3" + fdir "^6.2.0" is-reference "1.2.1" magic-string "^0.30.3" + picomatch "^4.0.2" "@rollup/plugin-json@^6.1.0": version "6.1.0" @@ -2531,15 +2728,14 @@ dependencies: "@rollup/pluginutils" "^5.1.0" -"@rollup/plugin-node-resolve@^15.2.3": - version "15.2.3" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" - integrity sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ== +"@rollup/plugin-node-resolve@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz#b1a0594661f40d7b061d82136e847354ff85f211" + integrity sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg== dependencies: "@rollup/pluginutils" "^5.0.1" "@types/resolve" "1.20.2" deepmerge "^4.2.2" - is-builtin-module "^3.2.1" is-module "^1.0.0" resolve "^1.22.1" @@ -2552,10 +2748,10 @@ smob "^1.0.0" terser "^5.17.4" -"@rollup/plugin-typescript@^11.1.6": - version "11.1.6" - resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz#724237d5ec12609ec01429f619d2a3e7d4d1b22b" - integrity sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA== +"@rollup/plugin-typescript@^12.1.2": + version "12.1.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz#ebaeec2e7376faa889030ccd7cb485a649e63118" + integrity sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg== dependencies: "@rollup/pluginutils" "^5.1.0" resolve "^1.22.1" @@ -2827,6 +3023,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + "@types/fbt@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@types/fbt/-/fbt-1.0.4.tgz#0d9e427f91fcff46bdcf2ca42a63343096565451" @@ -2906,7 +3107,7 @@ "@types/tough-cookie" "*" parse5 "^7.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.12": +"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2921,6 +3122,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.19.tgz#ad83aa9b7af470fab7e0f562be87e97dc8ffe08e" integrity sha512-Sq1itGUKUX1ap7GgZlrzdBydjbsJL/NSQt/4wkAxUJ7/OS5c2WkoN6WSpWc2Yc5wtKMZOUA0VCs/j2XJadN3HA== +"@types/node@^20": + version "20.17.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.10.tgz#3f7166190aece19a0d1d364d75c8b0b5778c1e18" + integrity sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA== + dependencies: + undici-types "~6.19.2" + "@types/node@^20.2.5": version "20.2.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb" @@ -3003,6 +3211,21 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz#992e5ac1553ce20d0d46aa6eccd79dc36dedc805" + integrity sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/type-utils" "8.18.1" + "@typescript-eslint/utils" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^1.3.0" + "@typescript-eslint/eslint-plugin@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz#de61c3083842fc6ac889d2fc83c9a96b55ab8328" @@ -3035,6 +3258,17 @@ natural-compare "^1.4.0" ts-api-utils "^1.3.0" +"@typescript-eslint/parser@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.1.tgz#c258bae062778b7696793bc492249027a39dfb95" + integrity sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA== + dependencies: + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" + debug "^4.3.4" + "@typescript-eslint/parser@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.4.0.tgz#540f4321de1e52b886c0fa68628af1459954c1f1" @@ -3065,6 +3299,14 @@ "@typescript-eslint/types" "7.4.0" "@typescript-eslint/visitor-keys" "7.4.0" +"@typescript-eslint/scope-manager@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz#52cedc3a8178d7464a70beffed3203678648e55b" + integrity sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ== + dependencies: + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" + "@typescript-eslint/scope-manager@8.7.0": version "8.7.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz#90ee7bf9bc982b9260b93347c01a8bc2b595e0b8" @@ -3083,6 +3325,16 @@ debug "^4.3.4" ts-api-utils "^1.0.1" +"@typescript-eslint/type-utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz#10f41285475c0bdee452b79ff7223f0e43a7781e" + integrity sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ== + dependencies: + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/utils" "8.18.1" + debug "^4.3.4" + ts-api-utils "^1.3.0" + "@typescript-eslint/type-utils@8.7.0": version "8.7.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz#d56b104183bdcffcc434a23d1ce26cde5e42df93" @@ -3098,6 +3350,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.4.0.tgz#ee9dafa75c99eaee49de6dcc9348b45d354419b6" integrity sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw== +"@typescript-eslint/types@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.1.tgz#d7f4f94d0bba9ebd088de840266fcd45408a8fff" + integrity sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw== + "@typescript-eslint/types@8.7.0": version "8.7.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.7.0.tgz#21d987201c07b69ce7ddc03451d7196e5445ad19" @@ -3117,6 +3374,20 @@ semver "^7.5.4" ts-api-utils "^1.0.1" +"@typescript-eslint/typescript-estree@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz#2a86cd64b211a742f78dfa7e6f4860413475367e" + integrity sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg== + dependencies: + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + "@typescript-eslint/typescript-estree@8.7.0": version "8.7.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz#6c7db6baa4380b937fa81466c546d052f362d0e8" @@ -3144,6 +3415,16 @@ "@typescript-eslint/typescript-estree" "7.4.0" semver "^7.5.4" +"@typescript-eslint/utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.1.tgz#c4199ea23fc823c736e2c96fd07b1f7235fa92d5" + integrity sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/utils@8.7.0": version "8.7.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.7.0.tgz#cef3f70708b5b5fd7ed8672fc14714472bd8a011" @@ -3162,6 +3443,14 @@ "@typescript-eslint/types" "7.4.0" eslint-visitor-keys "^3.4.1" +"@typescript-eslint/visitor-keys@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz#344b4f6bc83f104f514676facf3129260df7610a" + integrity sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ== + dependencies: + "@typescript-eslint/types" "8.18.1" + eslint-visitor-keys "^4.2.0" + "@typescript-eslint/visitor-keys@8.7.0": version "8.7.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz#5e46f1777f9d69360a883c1a56ac3c511c9659a8" @@ -3208,6 +3497,11 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + acorn@^8.4.1, acorn@^8.7.1: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" @@ -3621,11 +3915,6 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -builtin-modules@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" - integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== - call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -3841,6 +4130,16 @@ core-js-compat@^3.30.1, core-js-compat@^3.30.2: dependencies: browserslist "^4.21.5" +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" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -3855,6 +4154,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" @@ -4076,6 +4384,11 @@ entities@^4.4.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== +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" @@ -4088,6 +4401,36 @@ es5-ext@0.8.x: resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.8.2.tgz#aba8d9e1943a895ac96837a62a39b3f55ecd94ab" integrity sha512-H19ompyhnKiBdjHR1DPHvf5RHgHPmJaY9JNzFGbMbPgdsUkvnUCN1Ke8J4Y0IMyTwFM2M9l4h2GoHwzwpSmXbA== +esbuild@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.0.tgz#f2d470596885fcb2e91c21eb3da3b3c89c0b55e7" + integrity sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.24.0" + "@esbuild/android-arm" "0.24.0" + "@esbuild/android-arm64" "0.24.0" + "@esbuild/android-x64" "0.24.0" + "@esbuild/darwin-arm64" "0.24.0" + "@esbuild/darwin-x64" "0.24.0" + "@esbuild/freebsd-arm64" "0.24.0" + "@esbuild/freebsd-x64" "0.24.0" + "@esbuild/linux-arm" "0.24.0" + "@esbuild/linux-arm64" "0.24.0" + "@esbuild/linux-ia32" "0.24.0" + "@esbuild/linux-loong64" "0.24.0" + "@esbuild/linux-mips64el" "0.24.0" + "@esbuild/linux-ppc64" "0.24.0" + "@esbuild/linux-riscv64" "0.24.0" + "@esbuild/linux-s390x" "0.24.0" + "@esbuild/linux-x64" "0.24.0" + "@esbuild/netbsd-x64" "0.24.0" + "@esbuild/openbsd-arm64" "0.24.0" + "@esbuild/openbsd-x64" "0.24.0" + "@esbuild/sunos-x64" "0.24.0" + "@esbuild/win32-arm64" "0.24.0" + "@esbuild/win32-ia32" "0.24.0" + "@esbuild/win32-x64" "0.24.0" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -4133,6 +4476,14 @@ eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" +eslint-scope@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442" + integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-visitor-keys@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" @@ -4148,6 +4499,11 @@ eslint-visitor-keys@^3.4.3: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + eslint@8.57.0: version "8.57.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" @@ -4236,6 +4592,55 @@ eslint@^8.57.1: strip-ansi "^6.0.1" text-table "^0.2.0" +eslint@^9.13.0: + version "9.17.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.17.0.tgz#faa1facb5dd042172fdc520106984b5c2421bb0c" + integrity sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.19.0" + "@eslint/core" "^0.9.0" + "@eslint/eslintrc" "^3.2.0" + "@eslint/js" "9.17.0" + "@eslint/plugin-kit" "^0.2.3" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.1" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.2.0" + eslint-visitor-keys "^4.2.0" + espree "^10.3.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^10.0.1, espree@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" + espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -4257,6 +4662,13 @@ esquery@^1.4.2: dependencies: estraverse "^5.1.0" +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -4408,6 +4820,11 @@ fbt@^1.0.2: dependencies: invariant "^2.2.4" +fdir@^6.2.0: + version "6.4.2" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" + integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -4415,6 +4832,13 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -4462,11 +4886,24 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + flatted@^3.1.0: version "3.2.7" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flatted@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" + integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== + flow-enums-runtime@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.4.tgz#038635c679030d08d4c197db29a2fad62722072f" @@ -4592,17 +5029,6 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.3: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -4615,6 +5041,11 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -4758,7 +5189,7 @@ ignore@^5.3.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -import-fresh@^3.2.1: +import-fresh@^3.2.1, 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== @@ -4804,13 +5235,6 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-builtin-module@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" - integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== - dependencies: - builtin-modules "^3.3.0" - is-core-module@^2.11.0: version "2.12.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" @@ -6154,6 +6578,11 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-diff@^0.5.4: version "0.5.5" resolved "https://registry.yarnpkg.com/json-diff/-/json-diff-0.5.5.tgz#24658ad200dbdd64ae8a56baf4d87b2b33d7196e" @@ -6195,6 +6624,13 @@ keypress@~0.2.1: resolved "https://registry.yarnpkg.com/keypress/-/keypress-0.2.1.tgz#1e80454250018dbad4c3fe94497d6e67b6269c77" integrity sha512-HjorDJFNhnM4SicvaUXac0X77NiskggxJdesG72+O5zBKpSqKFCrqmndKVqpu3pFqkla0St6uGk8Ju0sCurrmg== +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -6429,13 +6865,6 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@~5.1.2: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - minimatch@^9.0.4: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" @@ -6443,6 +6872,13 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" +minimatch@~5.1.2: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -6724,6 +7160,11 @@ picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -7576,11 +8017,25 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +typescript-eslint@^8.16.0: + version "8.18.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.18.1.tgz#197b284b6769678ed77d9868df180eeaf61108eb" + integrity sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ== + dependencies: + "@typescript-eslint/eslint-plugin" "8.18.1" + "@typescript-eslint/parser" "8.18.1" + "@typescript-eslint/utils" "8.18.1" + typescript@^5.4.3: version "5.4.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" diff --git a/fixtures/dom/src/__tests__/nested-act-test.js b/fixtures/dom/src/__tests__/nested-act-test.js index 6c7f60c2e2..4a26e272cb 100644 --- a/fixtures/dom/src/__tests__/nested-act-test.js +++ b/fixtures/dom/src/__tests__/nested-act-test.js @@ -14,8 +14,6 @@ let TestAct; global.__DEV__ = process.env.NODE_ENV !== 'production'; -expect.extend(require('../toWarnDev')); - describe('unmocked scheduler', () => { beforeEach(() => { jest.resetModules(); diff --git a/fixtures/dom/src/toWarnDev.js b/fixtures/dom/src/toWarnDev.js deleted file mode 100644 index 6e275241e9..0000000000 --- a/fixtures/dom/src/toWarnDev.js +++ /dev/null @@ -1,284 +0,0 @@ -// copied from scripts/jest/matchers/toWarnDev.js -'use strict'; - -const {diff: jestDiff} = require('jest-diff'); -const util = require('util'); - -function shouldIgnoreConsoleError(format, args) { - if (__DEV__) { - if (typeof format === 'string') { - if (format.indexOf('The above error occurred') === 0) { - // This looks like an error addendum from ReactFiberErrorLogger. - // Ignore it too. - return true; - } - } - } else { - if ( - format != null && - typeof format.message === 'string' && - typeof format.stack === 'string' && - args.length === 0 - ) { - // In production, ReactFiberErrorLogger logs error objects directly. - // They are noisy too so we'll try to ignore them. - return true; - } - } - // Looks legit - return false; -} - -function normalizeCodeLocInfo(str) { - return str && str.replace(/at .+?:\d+/g, 'at **'); -} - -const createMatcherFor = consoleMethod => - function matcher(callback, expectedMessages, options = {}) { - if (__DEV__) { - // Warn about incorrect usage of matcher. - if (typeof expectedMessages === 'string') { - expectedMessages = [expectedMessages]; - } else if (!Array.isArray(expectedMessages)) { - throw Error( - `toWarnDev() requires a parameter of type string or an array of strings ` + - `but was given ${typeof expectedMessages}.` - ); - } - if ( - options != null && - (typeof options !== 'object' || Array.isArray(options)) - ) { - throw new Error( - 'toWarnDev() second argument, when present, should be an object. ' + - 'Did you forget to wrap the messages into an array?' - ); - } - if (arguments.length > 3) { - // `matcher` comes from Jest, so it's more than 2 in practice - throw new Error( - 'toWarnDev() received more than two arguments. ' + - 'Did you forget to wrap the messages into an array?' - ); - } - - const withoutStack = options.withoutStack; - const warningsWithoutComponentStack = []; - const warningsWithComponentStack = []; - const unexpectedWarnings = []; - - let lastWarningWithMismatchingFormat = null; - let lastWarningWithExtraComponentStack = null; - - // Catch errors thrown by the callback, - // But only rethrow them if all test expectations have been satisfied. - // Otherwise an Error in the callback can mask a failed expectation, - // and result in a test that passes when it shouldn't. - let caughtError; - - const isLikelyAComponentStack = message => - typeof message === 'string' && message.includes('\n in '); - - const consoleSpy = (format, ...args) => { - // Ignore uncaught errors reported by jsdom - // and React addendums because they're too noisy. - if ( - consoleMethod === 'error' && - shouldIgnoreConsoleError(format, args) - ) { - return; - } - - const message = util.format(format, ...args); - const normalizedMessage = normalizeCodeLocInfo(message); - - // Remember if the number of %s interpolations - // doesn't match the number of arguments. - // We'll fail the test if it happens. - let argIndex = 0; - format.replace(/%s/g, () => argIndex++); - if (argIndex !== args.length) { - lastWarningWithMismatchingFormat = { - format, - args, - expectedArgCount: argIndex, - }; - } - - // Protect against accidentally passing a component stack - // to warning() which already injects the component stack. - if ( - args.length >= 2 && - isLikelyAComponentStack(args[args.length - 1]) && - isLikelyAComponentStack(args[args.length - 2]) - ) { - lastWarningWithExtraComponentStack = { - format, - }; - } - - for (let index = 0; index < expectedMessages.length; index++) { - const expectedMessage = expectedMessages[index]; - if ( - normalizedMessage === expectedMessage || - normalizedMessage.includes(expectedMessage) - ) { - if (isLikelyAComponentStack(normalizedMessage)) { - warningsWithComponentStack.push(normalizedMessage); - } else { - warningsWithoutComponentStack.push(normalizedMessage); - } - expectedMessages.splice(index, 1); - return; - } - } - - let errorMessage; - if (expectedMessages.length === 0) { - errorMessage = - 'Unexpected warning recorded: ' + - this.utils.printReceived(normalizedMessage); - } else if (expectedMessages.length === 1) { - errorMessage = - 'Unexpected warning recorded: ' + - jestDiff(expectedMessages[0], normalizedMessage); - } else { - errorMessage = - 'Unexpected warning recorded: ' + - jestDiff(expectedMessages, [normalizedMessage]); - } - - // Record the call stack for unexpected warnings. - // We don't throw an Error here though, - // Because it might be suppressed by ReactFiberScheduler. - unexpectedWarnings.push(new Error(errorMessage)); - }; - - // TODO Decide whether we need to support nested toWarn* expectations. - // If we don't need it, add a check here to see if this is already our spy, - // And throw an error. - const originalMethod = console[consoleMethod]; - - // Avoid using Jest's built-in spy since it can't be removed. - console[consoleMethod] = consoleSpy; - - try { - callback(); - } catch (error) { - caughtError = error; - } finally { - // Restore the unspied method so that unexpected errors fail tests. - console[consoleMethod] = originalMethod; - - // Any unexpected Errors thrown by the callback should fail the test. - // This should take precedence since unexpected errors could block warnings. - if (caughtError) { - throw caughtError; - } - - // Any unexpected warnings should be treated as a failure. - if (unexpectedWarnings.length > 0) { - return { - message: () => unexpectedWarnings[0].stack, - pass: false, - }; - } - - // Any remaining messages indicate a failed expectations. - if (expectedMessages.length > 0) { - return { - message: () => - `Expected warning was not recorded:\n ${this.utils.printReceived( - expectedMessages[0] - )}`, - pass: false, - }; - } - - if (typeof withoutStack === 'number') { - // We're expecting a particular number of warnings without stacks. - if (withoutStack !== warningsWithoutComponentStack.length) { - return { - message: () => - `Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` + - warningsWithoutComponentStack.map(warning => - this.utils.printReceived(warning) - ), - pass: false, - }; - } - } else if (withoutStack === true) { - // We're expecting that all warnings won't have the stack. - // If some warnings have it, it's an error. - if (warningsWithComponentStack.length > 0) { - return { - message: () => - `Received warning unexpectedly includes a component stack:\n ${this.utils.printReceived( - warningsWithComponentStack[0] - )}\nIf this warning intentionally includes the component stack, remove ` + - `{withoutStack: true} from the toWarnDev() call. If you have a mix of ` + - `warnings with and without stack in one toWarnDev() call, pass ` + - `{withoutStack: N} where N is the number of warnings without stacks.`, - pass: false, - }; - } - } else if (withoutStack === false || withoutStack === undefined) { - // We're expecting that all warnings *do* have the stack (default). - // If some warnings don't have it, it's an error. - if (warningsWithoutComponentStack.length > 0) { - return { - message: () => - `Received warning unexpectedly does not include a component stack:\n ${this.utils.printReceived( - warningsWithoutComponentStack[0] - )}\nIf this warning intentionally omits the component stack, add ` + - `{withoutStack: true} to the toWarnDev() call.`, - pass: false, - }; - } - } else { - throw Error( - `The second argument for toWarnDev(), when specified, must be an object. It may have a ` + - `property called "withoutStack" whose value may be undefined, boolean, or a number. ` + - `Instead received ${typeof withoutStack}.` - ); - } - - if (lastWarningWithMismatchingFormat !== null) { - return { - message: () => - `Received ${ - lastWarningWithMismatchingFormat.args.length - } arguments for a message with ${ - lastWarningWithMismatchingFormat.expectedArgCount - } placeholders:\n ${this.utils.printReceived( - lastWarningWithMismatchingFormat.format - )}`, - pass: false, - }; - } - - if (lastWarningWithExtraComponentStack !== null) { - return { - message: () => - `Received more than one component stack for a warning:\n ${this.utils.printReceived( - lastWarningWithExtraComponentStack.format - )}\nDid you accidentally pass a stack to warning() as the last argument? ` + - `Don't forget warning() already injects the component stack automatically.`, - pass: false, - }; - } - - return {pass: true}; - } - } else { - // Any uncaught errors or warnings should fail tests in production mode. - callback(); - - return {pass: true}; - } - }; - -module.exports = { - toLowPriorityWarnDev: createMatcherFor('warn'), - toWarnDev: createMatcherFor('error'), -}; diff --git a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js index 0e9cb549f6..554e6e4bfb 100644 --- a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js +++ b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js @@ -22,6 +22,7 @@ let waitForPaint; let assertLog; let waitForThrow; let act; +let assertConsoleErrorDev; describe('ReactCache', () => { beforeEach(() => { @@ -39,6 +40,7 @@ describe('ReactCache', () => { assertLog = InternalTestUtils.assertLog; waitForThrow = InternalTestUtils.waitForThrow; waitForPaint = InternalTestUtils.waitForPaint; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; act = InternalTestUtils.act; TextResource = createResource( @@ -190,20 +192,31 @@ describe('ReactCache', () => { ); if (__DEV__) { - await expect(async () => { - await waitForAll([ - 'App', - 'Loading...', + await waitForAll([ + 'App', + 'Loading...', - ...(gate('enableSiblingPrerendering') ? ['App'] : []), - ]); - }).toErrorDev([ + ...(gate('enableSiblingPrerendering') ? ['App'] : []), + ]); + assertConsoleErrorDev([ 'Invalid key type. Expected a string, number, symbol, or ' + "boolean, but instead received: [ 'Hi', 100 ]\n\n" + 'To use non-primitive values as keys, you must pass a hash ' + - 'function as the second argument to createResource().', + 'function as the second argument to createResource().\n' + + ' in App (at **)' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in Suspense (at **)'), - ...(gate('enableSiblingPrerendering') ? ['Invalid key type'] : []), + ...(gate('enableSiblingPrerendering') + ? [ + 'Invalid key type. Expected a string, number, symbol, or ' + + "boolean, but instead received: [ 'Hi', 100 ]\n\n" + + 'To use non-primitive values as keys, you must pass a hash ' + + 'function as the second argument to createResource().\n' + + ' in App (at **)', + ] + : []), ]); } else { await waitForAll([ diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index b248970539..980dbbf0e1 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1467,13 +1467,12 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(); - await expect(async () => { - await act(() => { - startTransition(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); - }); + await act(() => { + startTransition(() => { + ReactNoop.render(ReactNoopFlightClient.read(transport)); }); - }).toErrorDev( + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.\n' + '\n' + 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' + @@ -1483,7 +1482,7 @@ describe('ReactFlight', () => { ? '' : ' in Indirection (at **)\n') + ' in App (at **)', - ); + ]); }); it('should trigger the inner most error boundary inside a Client Component', async () => { @@ -1541,17 +1540,47 @@ describe('ReactFlight', () => { return 123; }, }; - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported. ' + - 'Convert it manually to a simple value before passing it to props.\n' + - ' \n' + - ' ^^^^^^^^^^^^^^^', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a toJSON instance is passed to a host component child', () => { @@ -1560,43 +1589,123 @@ describe('ReactFlight', () => { return 123; } } - expect(() => { - const transport = ReactNoopFlightServer.render( -
Womp womp: {new MyError('spaghetti')}
, - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + - '
Womp womp: {Error}
\n' + - ' ^^^^^^^', - {withoutStack: true}, + const transport = ReactNoopFlightServer.render( +
Womp womp: {new MyError('spaghetti')}
, ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a host component', () => { - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' \n' + - ' ^^^^^^', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if an object with symbols is passed to a host component', () => { - expect(() => { - const transport = ReactNoopFlightServer.render( - , - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with symbol properties like Symbol.iterator are not supported.', - {withoutStack: true}, + const transport = ReactNoopFlightServer.render( + , ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a toJSON instance is passed to a Client Component', () => { @@ -1609,14 +1718,47 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported.', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => { @@ -1629,19 +1771,49 @@ describe('ReactFlight', () => { return
{children}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - Current date: {obj}, - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported. ' + - 'Convert it manually to a simple value before passing it to props.\n' + - ' <>Current date: {{toJSON: ...}}\n' + - ' ^^^^^^^^^^^^^^^', - {withoutStack: true}, + const transport = ReactNoopFlightServer.render( + Current date: {obj}, ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a Client Component', () => { @@ -1649,16 +1821,44 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' <... value={Math}>\n' + - ' ^^^^^^', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if an object with symbols is passed to a Client Component', () => { @@ -1666,16 +1866,46 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - , - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with symbol properties like Symbol.iterator are not supported.', - {withoutStack: true}, + assertConsoleErrorDev([]); + const transport = ReactNoopFlightServer.render( + , ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^\n', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a nested object in Client Component', () => { @@ -1683,18 +1913,41 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - hi}} />, - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' {hello: Math, title:

}\n' + - ' ^^^^', - {withoutStack: true}, + const transport = ReactNoopFlightServer.render( + , ); + ReactNoopFlightClient.read(transport); + + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + {withoutStack: true}, + ], + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a nested array in Client Component', () => { @@ -1702,20 +1955,40 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - hi

]} - />, - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' [..., Math,

]\n' + - ' ^^^^', - {withoutStack: true}, + const transport = ReactNoopFlightServer.render( + hi

]} />, ); + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + {withoutStack: true}, + ], + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should NOT warn in DEV for key getters', () => { @@ -1729,63 +2002,100 @@ describe('ReactFlight', () => { key: "this has a key but parent doesn't", }); } - expect(() => { - // While we're on the server we need to have the Server version active to track component stacks. - jest.resetModules(); - jest.mock('react', () => ReactServer); - const transport = ReactNoopFlightServer.render( - ReactServer.createElement( - 'div', - null, - Array(6).fill(ReactServer.createElement(NoKey)), - ), - ); - jest.resetModules(); - jest.mock('react', () => React); - ReactNoopFlightClient.read(transport); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + // While we're on the server we need to have the Server version active to track component stacks. + jest.resetModules(); + jest.mock('react', () => ReactServer); + const transport = ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + Array(6).fill(ReactServer.createElement(NoKey)), + ), + ); + jest.resetModules(); + jest.mock('react', () => React); + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in NoKey (at **)', + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in NoKey (at **)', + ]); + } else { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in NoKey (at **)', + ]); + } }); // @gate !__DEV__ || enableOwnerStacks it('should warn in DEV a child is missing keys on a fragment', () => { - expect(() => { - // While we're on the server we need to have the Server version active to track component stacks. - jest.resetModules(); - jest.mock('react', () => ReactServer); - const transport = ReactNoopFlightServer.render( - ReactServer.createElement( - 'div', - null, - Array(6).fill(ReactServer.createElement(ReactServer.Fragment)), - ), - ); - jest.resetModules(); - jest.mock('react', () => React); - ReactNoopFlightClient.read(transport); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + // While we're on the server we need to have the Server version active to track component stacks. + jest.resetModules(); + jest.mock('react', () => ReactServer); + const transport = ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + Array(6).fill(ReactServer.createElement(ReactServer.Fragment)), + ), + ); + jest.resetModules(); + jest.mock('react', () => React); + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Fragment (at **)', + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Fragment (at **)', + ]); + } else { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Fragment (at **)', + ]); + } }); it('should warn in DEV a child is missing keys in client component', async () => { function ParentClient({children}) { return children; } - const Parent = clientReference(ParentClient); - await expect(async () => { + + await act(async () => { + const Parent = clientReference(ParentClient); const transport = ReactNoopFlightServer.render( {Array(6).fill(
no key
)}
, ); ReactNoopFlightClient.read(transport); - await act(async () => { - ReactNoop.render(await ReactNoopFlightClient.read(transport)); - }); - }).toErrorDev( - gate(flags => flags.enableOwnerStacks) - ? 'Each child in a list should have a unique "key" prop.' + - '\n\nCheck the top-level render call using . ' + - 'See https://react.dev/link/warning-keys for more information.' - : 'Each child in a list should have a unique "key" prop. ' + - 'See https://react.dev/link/warning-keys for more information.', - ); + + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } else { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } }); it('should error if a class instance is passed to a host component', () => { @@ -3135,19 +3445,17 @@ describe('ReactFlight', () => { }, ); - let transport; - expect(() => { - // Reset the modules so that we get a new overridden console on top of the - // one installed by expect. This ensures that we still emit console.error - // calls. - jest.resetModules(); - jest.mock('react', () => require('react/react.react-server')); - ReactServer = require('react'); - ReactNoopFlightServer = require('react-noop-renderer/flight-server'); - transport = ReactNoopFlightServer.render({ - root: ReactServer.createElement(App), - }); - }).toErrorDev('err'); + // Reset the modules so that we get a new overridden console on top of the + // one installed by expect. This ensures that we still emit console.error + // calls. + jest.resetModules(); + jest.mock('react', () => require('react/react.react-server')); + ReactServer = require('react'); + ReactNoopFlightServer = require('react-noop-renderer/flight-server'); + const transport = ReactNoopFlightServer.render({ + root: ReactServer.createElement(App), + }); + assertConsoleErrorDev(['Error: err']); expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog.mock.calls[0][0]).toBe('hi'); diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index ff8e7e1ac8..87f98b99f2 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -14,6 +14,7 @@ let React; let ReactTestRenderer; let ReactDebugTools; let act; +let assertConsoleErrorDev; let useMemoCache; function normalizeSourceLoc(tree) { @@ -33,7 +34,7 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); ReactDebugTools = require('react-debug-tools'); useMemoCache = require('react/compiler-runtime').c; }); @@ -2344,10 +2345,12 @@ describe('ReactHooksInspectionIntegration', () => { , ); - await expect(async () => { - await act(async () => await LazyFoo); - }).toErrorDev([ - 'Foo: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', + await act(async () => await LazyFoo); + assertConsoleErrorDev([ + 'Foo: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in Foo (at **)\n' + ' in Suspense (at **)'), ]); const childFiber = renderer.root._currentFiber(); diff --git a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js index 923ee1f5d8..5a43a9ec2f 100644 --- a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js +++ b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js @@ -14,6 +14,7 @@ describe('when Trusted Types are available in global object', () => { let ReactDOMClient; let ReactFeatureFlags; let act; + let assertConsoleErrorDev; let container; let ttObject1; let ttObject2; @@ -36,7 +37,7 @@ describe('when Trusted Types are available in global object', () => { ReactFeatureFlags.enableTrustedTypesIntegration = true; React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); ttObject1 = { toString() { return 'Hi'; @@ -208,17 +209,16 @@ describe('when Trusted Types are available in global object', () => { it('should warn once when rendering script tag in jsx on client', async () => { const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Encountered a script tag while rendering React component. ' + 'Scripts inside React components are never executed when rendering ' + 'on the client. Consider using template tag instead ' + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' + ' in script (at **)', - ); + ]); // check that the warning is printed only once await act(() => { diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 03f0cd0a6c..05116be301 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -16,6 +16,7 @@ let ReactNativePrivateInterface; let createReactNativeComponentClass; let StrictMode; let act; +let assertConsoleErrorDev; const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = "dispatchCommand was called with a ref that isn't a " + @@ -38,7 +39,7 @@ describe('ReactFabric', () => { createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') .ReactNativeViewConfigRegistry.register; - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('should be able to create and render a native component', async () => { @@ -459,9 +460,8 @@ describe('ReactFabric', () => { }); expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled(); - expect(() => { - ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]); - }).toErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { + ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + assertConsoleErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -525,9 +525,8 @@ describe('ReactFabric', () => { }); expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled(); - expect(() => { - ReactFabric.sendAccessibilityEvent(viewRef, 'eventTypeName'); - }).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { + ReactFabric.sendAccessibilityEvent(viewRef, 'eventTypeName'); + assertConsoleErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -856,24 +855,31 @@ describe('ReactFabric', () => { uiViewClassName: 'RCTView', })); - await expect(async () => { - await act(() => { - ReactFabric.render(this should warn, 11, null, true); - }); - }).toErrorDev(['Text strings must be rendered within a component.']); + await act(() => { + ReactFabric.render(this should warn, 11, null, true); + }); + assertConsoleErrorDev([ + 'Text strings must be rendered within a component.\n' + + ' in RCTView (at **)', + ]); - await expect(async () => { - await act(() => { - ReactFabric.render( - - hi hello hi - , - 11, - null, - true, - ); - }); - }).toErrorDev(['Text strings must be rendered within a component.']); + await act(() => { + ReactFabric.render( + + hi hello hi + , + 11, + null, + true, + ); + }); + assertConsoleErrorDev([ + 'Text strings must be rendered within a component.\n' + + ' in RCTScrollView (at **)' + + (gate(flags => !flags.enableOwnerStacks) + ? '\n in RCTText (at **)' + : ''), + ]); }); it('should not throw for text inside of an indirect ancestor', async () => { @@ -1166,10 +1172,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect( - () => (match = ReactFabric.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactFabric.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1207,10 +1211,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect( - () => (match = ReactFabric.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactFabric.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1250,8 +1252,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect(() => (match = ReactFabric.findNodeHandle(parent))).toErrorDev([ + const match = ReactFabric.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1291,8 +1293,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect(() => (match = ReactFabric.findNodeHandle(parent))).toErrorDev([ + const match = ReactFabric.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1313,16 +1315,16 @@ describe('ReactFabric', () => { return null; } } - await expect(async () => { - await act(() => { - ReactFabric.render(, 11, null, true); - }); - }).toErrorDev([ + await act(() => { + ReactFabric.render(, 11, null, true); + }); + assertConsoleErrorDev([ 'TestComponent is accessing findNodeHandle inside its render(). ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + - 'componentDidUpdate instead.', + 'componentDidUpdate instead.\n' + + ' in TestComponent (at **)', ]); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js index 46b2ad9cf1..f4ab24d71d 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js @@ -18,6 +18,7 @@ let ReactNative; let ResponderEventPlugin; let UIManager; let createReactNativeComponentClass; +let assertConsoleErrorDev; // Parallels requireNativeComponent() in that it lazily constructs a view config, // And registers view manager event types with ReactNativeViewConfigRegistry. @@ -69,6 +70,7 @@ beforeEach(() => { require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').RCTEventEmitter; React = require('react'); act = require('internal-test-utils').act; + assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev; ReactNative = require('react-native-renderer'); ResponderEventPlugin = require('react-native-renderer/src/legacy-events/ResponderEventPlugin').default; @@ -227,30 +229,32 @@ test('handles events on text nodes', () => { } const log = []; - expect(() => { - ReactNative.render( - - - log.push('string touchend')} - onTouchEndCapture={() => log.push('string touchend capture')} - onTouchStart={() => log.push('string touchstart')} - onTouchStartCapture={() => log.push('string touchstart capture')}> - Text Content - - log.push('number touchend')} - onTouchEndCapture={() => log.push('number touchend capture')} - onTouchStart={() => log.push('number touchstart')} - onTouchStartCapture={() => log.push('number touchstart capture')}> - {123} - + ReactNative.render( + + + log.push('string touchend')} + onTouchEndCapture={() => log.push('string touchend capture')} + onTouchStart={() => log.push('string touchstart')} + onTouchStartCapture={() => log.push('string touchstart capture')}> + Text Content - , - 1, - ); - }).toErrorDev([ - 'ContextHack uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + log.push('number touchend')} + onTouchEndCapture={() => log.push('number touchend capture')} + onTouchStart={() => log.push('number touchstart')} + onTouchStartCapture={() => log.push('number touchstart capture')}> + {123} + + + , + 1, + ); + assertConsoleErrorDev([ + 'ContextHack uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. ' + + '(https://react.dev/link/legacy-context)' + + '\n in ContextHack (at **)', ]); expect(UIManager.createView).toHaveBeenCalledTimes(5); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index 790078b224..0aa1c3f0ba 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -18,6 +18,7 @@ let UIManager; let TextInputState; let ReactNativePrivateInterface; let act; +let assertConsoleErrorDev; const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = "dispatchCommand was called with a ref that isn't a " + @@ -32,7 +33,7 @@ describe('ReactNative', () => { jest.resetModules(); React = require('react'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); StrictMode = React.StrictMode; ReactNative = require('react-native-renderer'); ReactNativePrivateInterface = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'); @@ -158,9 +159,8 @@ describe('ReactNative', () => { ); expect(UIManager.dispatchViewManagerCommand).not.toBeCalled(); - expect(() => { - ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]); - }).toErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { + ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + assertConsoleErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -219,9 +219,8 @@ describe('ReactNative', () => { ); expect(UIManager.sendAccessibilityEvent).not.toBeCalled(); - expect(() => { - ReactNative.sendAccessibilityEvent(viewRef, 'updateCommand', [10, 20]); - }).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { + ReactNative.sendAccessibilityEvent(viewRef, 'updateCommand', [10, 20]); + assertConsoleErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -614,10 +613,8 @@ describe('ReactNative', () => { ReactNative.render( (parent = n)} />, 11); - let match; - expect( - () => (match = ReactNative.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactNative.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -652,10 +649,8 @@ describe('ReactNative', () => { 11, ); - let match; - expect( - () => (match = ReactNative.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactNative.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -689,8 +684,8 @@ describe('ReactNative', () => { ReactNative.render( (parent = n)} />, 11); - let match; - expect(() => (match = ReactNative.findNodeHandle(parent))).toErrorDev([ + const match = ReactNative.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -725,8 +720,8 @@ describe('ReactNative', () => { 11, ); - let match; - expect(() => (match = ReactNative.findNodeHandle(parent))).toErrorDev([ + const match = ReactNative.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index ddcd751957..1b665b2b8b 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -233,8 +233,28 @@ describe('ReactLazy', () => { expect(error.message).toMatch('Element type is invalid'); assertLog(['Loading...']); assertConsoleErrorDev([ - 'Expected the result of a dynamic import() call', - 'Expected the result of a dynamic import() call', + 'lazy: Expected the result of a dynamic import() call. ' + + 'Instead received: function Text(props) {\n' + + ' Scheduler.log(props.text);\n' + + ' return props.text;\n' + + ' }\n\n' + + 'Your code should look like: \n ' + + "const MyComponent = lazy(() => import('./MyComponent'))\n" + + (gate('enableOwnerStacks') + ? '' + : ' in Lazy (at **)\n' + ' in Suspense (at **)\n') + + ' in App (at **)', + 'lazy: Expected the result of a dynamic import() call. ' + + 'Instead received: function Text(props) {\n' + + ' Scheduler.log(props.text);\n' + + ' return props.text;\n' + + ' }\n\n' + + 'Your code should look like: \n ' + + "const MyComponent = lazy(() => import('./MyComponent'))\n" + + (gate('enableOwnerStacks') + ? '' + : ' in Lazy (at **)\n' + ' in Suspense (at **)\n') + + ' in App (at **)', ]); expect(root).not.toMatchRenderedOutput('Hi'); }); @@ -852,19 +872,21 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('22'); // Mount - await expect(async () => { - await act(() => resolveFakeImport(Add)); - }).toErrorDev( - shouldWarnAboutFunctionDefaultProps - ? [ - 'Add: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', - ] - : shouldWarnAboutMemoDefaultProps - ? [ - 'Add: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', - ] - : [], - ); + await act(() => resolveFakeImport(Add)); + + if (shouldWarnAboutFunctionDefaultProps) { + assertConsoleErrorDev([ + 'Add: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.\n' + + ' in Add (at **)\n' + + ' in Suspense (at **)', + ]); + } else if (shouldWarnAboutMemoDefaultProps) { + assertConsoleErrorDev([ + 'Add: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.\n' + + ' in Suspense (at **)', + ]); + } + expect(root).toMatchRenderedOutput('22'); // Update diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js index 4dbba1bca2..2b5707b8c5 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js @@ -491,4 +491,46 @@ describe('ReactSuspenseyCommitPhase', () => { , ); }); + + // FIXME: Should pass with `enableYieldingBeforePassive` + // @gate !enableYieldingBeforePassive + it('runs passive effects after suspended commit resolves', async () => { + function Effect() { + React.useEffect(() => { + Scheduler.log('flush effect'); + }); + return ; + } + + const root = ReactNoop.createRoot(); + + await act(() => { + root.render( + }> + + + , + ); + }); + + assertLog([ + 'render effect', + 'Image requested [A]', + 'Loading...', + 'render effect', + ]); + expect(root).toMatchRenderedOutput('Loading...'); + + await act(() => { + resolveSuspenseyThing('A'); + }); + + assertLog(['flush effect']); + expect(root).toMatchRenderedOutput( + <> + {'render effect'} + + , + ); + }); }); diff --git a/packages/react-server-dom-parcel/npm/server.edge.js b/packages/react-server-dom-parcel/npm/server.edge.js index 5f13279f75..356cce93a7 100644 --- a/packages/react-server-dom-parcel/npm/server.edge.js +++ b/packages/react-server-dom-parcel/npm/server.edge.js @@ -9,6 +9,7 @@ if (process.env.NODE_ENV === 'production') { exports.renderToReadableStream = s.renderToReadableStream; exports.decodeReply = s.decodeReply; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; exports.decodeAction = s.decodeAction; exports.decodeFormState = s.decodeFormState; exports.createClientReference = s.createClientReference; diff --git a/packages/react-server-dom-parcel/server.edge.js b/packages/react-server-dom-parcel/server.edge.js index 0974db3448..42f5c3d653 100644 --- a/packages/react-server-dom-parcel/server.edge.js +++ b/packages/react-server-dom-parcel/server.edge.js @@ -10,6 +10,7 @@ export { renderToReadableStream, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, createClientReference, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js index 73a8741618..2a365993a7 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js @@ -17,6 +17,8 @@ import { type ServerReferenceId, } from '../client/ReactFlightClientConfigBundlerParcel'; +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + import { createRequest, createPrerenderRequest, @@ -30,6 +32,9 @@ import { createResponse, close, getRoot, + reportGlobalError, + resolveField, + resolveFile, } from 'react-server/src/ReactFlightReplyServer'; import { @@ -189,6 +194,50 @@ export function decodeReply( return root; } +export function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + serverManifest, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + export function decodeAction(body: FormData): Promise<() => T> | null { return decodeActionImpl(body, serverManifest); } diff --git a/packages/react-server-dom-parcel/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-parcel/src/server/react-flight-dom-server.edge.js index c6b3067fbc..54f3dbb2ec 100644 --- a/packages/react-server-dom-parcel/src/server/react-flight-dom-server.edge.js +++ b/packages/react-server-dom-parcel/src/server/react-flight-dom-server.edge.js @@ -11,6 +11,7 @@ export { renderToReadableStream, prerender as unstable_prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, createClientReference, diff --git a/packages/react-server-dom-turbopack/npm/server.edge.js b/packages/react-server-dom-turbopack/npm/server.edge.js index e34b18fa01..c832080079 100644 --- a/packages/react-server-dom-turbopack/npm/server.edge.js +++ b/packages/react-server-dom-turbopack/npm/server.edge.js @@ -9,6 +9,7 @@ if (process.env.NODE_ENV === 'production') { exports.renderToReadableStream = s.renderToReadableStream; exports.decodeReply = s.decodeReply; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; exports.decodeAction = s.decodeAction; exports.decodeFormState = s.decodeFormState; exports.registerServerReference = s.registerServerReference; diff --git a/packages/react-server-dom-turbopack/server.edge.js b/packages/react-server-dom-turbopack/server.edge.js index c527c7f76a..8f0347cd7b 100644 --- a/packages/react-server-dom-turbopack/server.edge.js +++ b/packages/react-server-dom-turbopack/server.edge.js @@ -10,6 +10,7 @@ export { renderToReadableStream, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, registerServerReference, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js index 11dbe1a7c1..e8256767fa 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js @@ -12,6 +12,8 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + import { createRequest, createPrerenderRequest, @@ -25,6 +27,9 @@ import { createResponse, close, getRoot, + reportGlobalError, + resolveField, + resolveFile, } from 'react-server/src/ReactFlightReplyServer'; import { @@ -183,10 +188,56 @@ function decodeReply( return root; } +function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + turbopackMap: ServerManifest, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + turbopackMap, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + export { renderToReadableStream, prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, }; diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js index 48c4fc4553..9198f9913e 100644 --- a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js @@ -11,6 +11,7 @@ export { renderToReadableStream, prerender as unstable_prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, registerServerReference, diff --git a/packages/react-server-dom-webpack/npm/server.edge.js b/packages/react-server-dom-webpack/npm/server.edge.js index 591b844768..51a58ea7a9 100644 --- a/packages/react-server-dom-webpack/npm/server.edge.js +++ b/packages/react-server-dom-webpack/npm/server.edge.js @@ -9,6 +9,7 @@ if (process.env.NODE_ENV === 'production') { exports.renderToReadableStream = s.renderToReadableStream; exports.decodeReply = s.decodeReply; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; exports.decodeAction = s.decodeAction; exports.decodeFormState = s.decodeFormState; exports.registerServerReference = s.registerServerReference; diff --git a/packages/react-server-dom-webpack/server.edge.js b/packages/react-server-dom-webpack/server.edge.js index c527c7f76a..8f0347cd7b 100644 --- a/packages/react-server-dom-webpack/server.edge.js +++ b/packages/react-server-dom-webpack/server.edge.js @@ -10,6 +10,7 @@ export { renderToReadableStream, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, registerServerReference, diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 3eccca1a9b..040bb046b5 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -38,6 +38,7 @@ let ReactServerDOM; let Scheduler; let ReactServerScheduler; let reactServerAct; +let assertConsoleErrorDev; describe('ReactFlightDOMBrowser', () => { beforeEach(() => { @@ -75,7 +76,7 @@ describe('ReactFlightDOMBrowser', () => { Scheduler = require('scheduler'); patchMessageChannel(Scheduler); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); @@ -1156,25 +1157,38 @@ describe('ReactFlightDOMBrowser', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - const stream = await serverAct(() => - ReactServerDOMServer.renderToReadableStream( - <> - {Array(6).fill(
no key
)}
- - {Array(6).fill(
no key
)} -
- , - webpackMap, - ), - ); - const result = - await ReactServerDOMClient.createFromReadableStream(stream); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + <> + {Array(6).fill(
no key
)}
+ + {Array(6).fill(
no key
)} +
+ , + webpackMap, + ), + ); + const result = await ReactServerDOMClient.createFromReadableStream(stream); - await act(() => { - root.render(result); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + if (!gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } + + await act(() => { + root.render(result); + }); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } }); it('basic use(promise)', async () => { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index f2814f250a..603dbbf09e 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -37,6 +37,7 @@ let ReactServerDOMStaticServer; let ReactServerDOMClient; let use; let reactServerAct; +let assertConsoleErrorDev; function normalizeCodeLocInfo(str) { return ( @@ -66,6 +67,8 @@ describe('ReactFlightDOMEdge', () => { jest.resetModules(); reactServerAct = require('internal-test-utils').serverAct; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); @@ -802,17 +805,19 @@ describe('ReactFlightDOMEdge', () => { ), }; - expect(() => { - ServerModule.greet.bind({}, 'hi'); - }).toErrorDev( - 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ServerModule.greet.bind({}, 'hi'); + assertConsoleErrorDev( + [ + 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ], {withoutStack: true}, ); - expect(() => { - ServerModuleImportedOnClient.greet.bind({}, 'hi'); - }).toErrorDev( - 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ServerModuleImportedOnClient.greet.bind({}, 'hi'); + assertConsoleErrorDev( + [ + 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ], {withoutStack: true}, ); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index 0b4549d5ba..bb7c2c955b 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -50,6 +50,7 @@ let ReactServerDOMClient; let ReactDOMClient; let useActionState; let act; +let assertConsoleErrorDev; describe('ReactFlightDOMForm', () => { beforeEach(() => { @@ -72,6 +73,8 @@ describe('ReactFlightDOMForm', () => { ReactDOMServer = require('react-dom/server.edge'); ReactDOMClient = require('react-dom/client'); act = React.act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; // TODO: Test the old api but it warns so needs warnings to be asserted. // if (__VARIANT__) { @@ -959,12 +962,13 @@ describe('ReactFlightDOMForm', () => { await readIntoContainer(postbackSsrStream); } - await expect(submitTheForm).toErrorDev( + await submitTheForm(); + assertConsoleErrorDev([ 'Failed to serialize an action for progressive enhancement:\n' + 'Error: React Element cannot be passed to Server Functions from the Client without a temporary reference set. Pass a TemporaryReferenceSet to the options.\n' + ' [
]\n' + ' ^^^^^^', - ); + ]); // The error message was returned as JSX. const form2 = container.getElementsByTagName('form')[0]; @@ -1035,10 +1039,11 @@ describe('ReactFlightDOMForm', () => { await readIntoContainer(postbackSsrStream); } - await expect(submitTheForm).toErrorDev( + await submitTheForm(); + assertConsoleErrorDev([ 'Failed to serialize an action for progressive enhancement:\n' + 'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.', - ); + ]); expect(blob instanceof Blob).toBe(true); expect(blob.size).toBe(2); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js index f6157dff17..2effa9868e 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js @@ -272,4 +272,40 @@ describe('ReactFlightDOMReplyEdge', () => { expect(error).not.toBe(null); expect(error.message).toBe('Connection closed.'); }); + + it('can stream the decoding using an async iterable', async () => { + let resolve; + const promise = new Promise(r => (resolve = r)); + + const buffer = new Uint8Array([ + 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, + ]); + + const formData = await ReactServerDOMClient.encodeReply({ + a: Promise.resolve('hello'), + b: Promise.resolve(buffer), + }); + + const iterable = { + async *[Symbol.asyncIterator]() { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const entry of formData) { + yield entry; + await promise; + } + }, + }; + + const decoded = await ReactServerDOMServer.decodeReplyFromAsyncIterable( + iterable, + webpackServerMap, + ); + + expect(Object.keys(decoded)).toEqual(['a', 'b']); + + await resolve(); + + expect(await decoded.a).toBe('hello'); + expect(Array.from(await decoded.b)).toEqual(Array.from(buffer)); + }); }); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js index 7954417b95..e5b834be05 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js @@ -12,6 +12,8 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + import { createRequest, createPrerenderRequest, @@ -25,6 +27,9 @@ import { createResponse, close, getRoot, + reportGlobalError, + resolveField, + resolveFile, } from 'react-server/src/ReactFlightReplyServer'; import { @@ -183,10 +188,56 @@ function decodeReply( return root; } +function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + webpackMap: ServerManifest, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + webpackMap, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + export { renderToReadableStream, prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, }; diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js index 48c4fc4553..9198f9913e 100644 --- a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js @@ -11,6 +11,7 @@ export { renderToReadableStream, prerender as unstable_prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, registerServerReference, diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js index e3400b173a..0b08cde378 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js @@ -14,6 +14,7 @@ let React; let ReactCache; let ReactTestRenderer; let act; +let assertConsoleErrorDev; describe('ReactTestRenderer', () => { beforeEach(() => { @@ -27,19 +28,27 @@ describe('ReactTestRenderer', () => { ReactTestRenderer = require('react-test-renderer'); const InternalTestUtils = require('internal-test-utils'); act = InternalTestUtils.act; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); it('should warn if used to render a ReactDOM portal', async () => { const container = document.createElement('div'); let error; - await expect(async () => { - await act(() => { - ReactTestRenderer.create(ReactDOM.createPortal('foo', container)); - }).catch(e => (error = e)); - }).toErrorDev('An invalid container has been provided.', { - withoutStack: true, - }); + await act(() => { + ReactTestRenderer.create(ReactDOM.createPortal('foo', container)); + }).catch(e => (error = e)); + assertConsoleErrorDev( + [ + 'An invalid container has been provided. ' + + 'This may indicate that another renderer is being used in addition to the test renderer. ' + + '(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' + + 'This is not supported.', + ], + { + withoutStack: true, + }, + ); // After the update throws, a subsequent render is scheduled to // unmount the whole tree. This update also causes an error, so React diff --git a/packages/react/src/__tests__/ReactChildren-test.js b/packages/react/src/__tests__/ReactChildren-test.js index 2723755abf..5cce6b2487 100644 --- a/packages/react/src/__tests__/ReactChildren-test.js +++ b/packages/react/src/__tests__/ReactChildren-test.js @@ -13,12 +13,13 @@ describe('ReactChildren', () => { let React; let ReactDOMClient; let act; + let assertConsoleErrorDev; beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('should support identity for simple', () => { @@ -331,14 +332,16 @@ describe('ReactChildren', () => { callback.mockClear(); } - let instance; - expect(() => { - instance =
{threeDivIterable}
; - }).toErrorDev( + const instance =
{threeDivIterable}
; + assertConsoleErrorDev( // With the flag on this doesn't warn eagerly but only when rendered gate(flag => flag.enableOwnerStacks) ? [] - : ['Each child in a list should have a unique "key" prop.'], + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ], ); React.Children.forEach(instance.props.children, callback, context); @@ -359,11 +362,16 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(instance); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + await act(() => { + root.render(instance); + }); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. It was passed a child from div.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)' + + (gate(flag => flag.enableOwnerStacks) ? '' : '\n in div (at **)'), + ]); }); it('should be called for each child in an iterable with keys', () => { @@ -879,15 +887,29 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - - {[
]} - , - ); - }); - }).toErrorDev(['Each child in a list should have a unique "key" prop.']); + await act(() => { + root.render( + + {[
]} + , + ); + }); + assertConsoleErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentRenderingMappedChildren`.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)\n' + + ' in **/ReactChildren-test.js:**:** (at **)', + ] + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using .' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ], + ); }); it('does not warn for mapped static children without keys', async () => { @@ -903,16 +925,14 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - -
-
- , - ); - }); - }).toErrorDev([]); + await act(() => { + root.render( + +
+
+ , + ); + }); }); it('warns for cloned list children without keys', async () => { @@ -926,15 +946,28 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - - {[
]} - , - ); - }); - }).toErrorDev(['Each child in a list should have a unique "key" prop.']); + await act(() => { + root.render( + + {[
]} + , + ); + }); + assertConsoleErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentRenderingClonedChildren`.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ] + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using .' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ], + ); }); it('does not warn for cloned static children without keys', async () => { @@ -948,16 +981,14 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - -
-
- , - ); - }); - }).toErrorDev([]); + await act(() => { + root.render( + +
+
+ , + ); + }); }); it('warns for flattened list children without keys', async () => { @@ -967,15 +998,28 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - - {[
]} - , - ); - }); - }).toErrorDev(['Each child in a list should have a unique "key" prop.']); + await act(() => { + root.render( + + {[
]} + , + ); + }); + assertConsoleErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentRenderingFlattenedChildren`.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ] + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using .' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ], + ); }); it('does not warn for flattened static children without keys', async () => { @@ -985,16 +1029,14 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - -
-
- , - ); - }); - }).toErrorDev([]); + await act(() => { + root.render( + +
+
+ , + ); + }); }); it('should escape keys', () => { @@ -1153,18 +1195,16 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '' + - 'Each child in a list should have a unique "key" prop.' + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the top-level render call using . It was passed a child from ComponentReturningArray. ' + 'See https://react.dev/link/warning-keys for more information.' + '\n in div (at **)' + '\n in ComponentReturningArray (at **)', - ); + ]); }); it('does not warn when there are keys on elements in a fragment', async () => { @@ -1184,17 +1224,15 @@ describe('ReactChildren', () => { it('warns for keys for arrays at the top level', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render([
,
]); - }); - }).toErrorDev( - '' + - 'Each child in a list should have a unique "key" prop.' + + await act(() => { + root.render([
,
]); + }); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the top-level render call using . ' + 'See https://react.dev/link/warning-keys for more information.' + '\n in div (at **)', - ); + ]); }); }); }); diff --git a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee index 3ad6aa0235..5fab9d7d0b 100644 --- a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee +++ b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee @@ -9,6 +9,8 @@ PropTypes = null React = null ReactDOM = null ReactDOMClient = null +assertConsoleErrorDev = null +assertConsoleWarnDev = null featureFlags = require 'shared/ReactFeatureFlags' @@ -28,6 +30,9 @@ describe 'ReactCoffeeScriptClass', -> root = ReactDOMClient.createRoot container attachedListener = null renderedName = null + TestUtils = require 'internal-test-utils' + assertConsoleErrorDev = TestUtils.assertConsoleErrorDev + assertConsoleWarnDev = TestUtils.assertConsoleWarnDev InnerComponent = class extends React.Component getName: -> this.props.name render: -> @@ -53,14 +58,15 @@ describe 'ReactCoffeeScriptClass', -> event.preventDefault() caughtErrors.push(event.error) window.addEventListener 'error', errorHandler; - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo) - ).toErrorDev([ - # A failed component renders twice in DEV in concurrent mode - 'No `render` method found on the Foo instance', - 'No `render` method found on the Foo instance', - ]) + ReactDOM.flushSync -> + root.render React.createElement(Foo) + assertConsoleErrorDev [ +# A failed component renders twice in DEV in concurrent mode + 'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' + + ' in Foo (at **)', + 'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' + + ' in Foo (at **)', + ] window.removeEventListener 'error', errorHandler; expect(caughtErrors).toEqual([ expect.objectContaining( @@ -136,11 +142,11 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('div') getDerivedStateFromProps: -> {} - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + assertConsoleErrorDev [ + 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)'] it 'warns if getDerivedStateFromError is not static', -> class Foo extends React.Component @@ -148,11 +154,13 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('div') getDerivedStateFromError: -> {} - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + + assertConsoleErrorDev [ + 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)' + ] it 'warns if getSnapshotBeforeUpdate is static', -> class Foo extends React.Component @@ -160,11 +168,13 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('div') Foo.getSnapshotBeforeUpdate = () -> {} - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.' + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + + assertConsoleErrorDev [ + 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)' + ] it 'warns if state not initialized before static getDerivedStateFromProps', -> class Foo extends React.Component @@ -177,16 +187,16 @@ describe 'ReactCoffeeScriptClass', -> foo: nextProps.foo bar: 'bar' } - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev ( - '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + - 'undefined. This is not recommended. Instead, define the initial state by ' + - 'assigning an object to `this.state` in the constructor of `Foo`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.' - ) + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + + assertConsoleErrorDev [ + '`Foo` uses `getDerivedStateFromProps` but its initial state is + undefined. This is not recommended. Instead, define the initial state by + assigning an object to `this.state` in the constructor of `Foo`. + This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Foo (at **)' + ] it 'updates initial state with values returned by static getDerivedStateFromProps', -> class Foo extends React.Component @@ -254,12 +264,28 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement Foo - expect(-> - test React.createElement(Outer), 'SPAN', 'foo' - ).toErrorDev([ - 'Outer uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Foo uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - ]) + test React.createElement(Outer), 'SPAN', 'foo' + + if featureFlags.enableOwnerStacks + assertConsoleErrorDev([ + 'Outer uses the legacy childContextTypes API which will soon be removed. + Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + 'Foo uses the legacy contextTypes API which will soon be removed. + Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + ]); + else + assertConsoleErrorDev([ + 'Outer uses the legacy childContextTypes API which will soon be removed. + Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + 'Foo uses the legacy contextTypes API which will soon be removed. + Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)\n' + + ' in Outer (at **)', + ]); + it 'renders only once when setting state in componentWillMount', -> renderCount = 0 @@ -286,9 +312,11 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement('span') - expect(-> - test React.createElement(Foo), 'SPAN', '' - ).toErrorDev('Foo.state: must be set to an object or null') + test React.createElement(Foo), 'SPAN', '' + assertConsoleErrorDev [ + 'Foo.state: must be set to an object or null\n' + + ' in Foo (at **)' + ] it 'should render with null in the initial state property', -> class Foo extends React.Component @@ -430,14 +458,21 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(Foo), 'SPAN', 'foo' - ).toErrorDev([ - 'getInitialState was defined on Foo, a plain JavaScript class.', - 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'contextTypes was defined as an instance property on Foo.', - 'contextType was defined as an instance property on Foo.', - ]) + test React.createElement(Foo), 'SPAN', 'foo' + assertConsoleErrorDev [ + 'getInitialState was defined on Foo, a plain JavaScript class. + This is only supported for classes created using React.createClass. + Did you mean to define a state property instead?\n' + + ' in Foo (at **)', + 'getDefaultProps was defined on Foo, a plain JavaScript class. + This is only supported for classes created using React.createClass. + Use a static property to define defaultProps instead.\n' + + ' in Foo (at **)', + 'contextType was defined as an instance property on Foo. Use a static property to define contextType instead.\n' + + ' in Foo (at **)', + 'contextTypes was defined as an instance property on Foo. Use a static property to define contextTypes instead.\n' + + ' in Foo (at **)', + ] expect(getInitialStateWasCalled).toBe false expect(getDefaultPropsWasCalled).toBe false @@ -468,13 +503,13 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(NamedComponent), 'SPAN', 'foo' - ).toErrorDev( + test React.createElement(NamedComponent), 'SPAN', 'foo' + assertConsoleErrorDev [ 'NamedComponent has a method called componentShouldUpdate(). Did you mean shouldComponentUpdate()? The name is phrased as a - question because the function is expected to return a value.' - ) + question because the function is expected to return a value.\n' + + ' in NamedComponent (at **)' + ] it 'should warn when misspelling componentWillReceiveProps', -> class NamedComponent extends React.Component @@ -486,12 +521,12 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(NamedComponent), 'SPAN', 'foo' - ).toErrorDev( + test React.createElement(NamedComponent), 'SPAN', 'foo' + assertConsoleErrorDev [ 'NamedComponent has a method called componentWillRecieveProps(). - Did you mean componentWillReceiveProps()?' - ) + Did you mean componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)' + ] it 'should warn when misspelling UNSAFE_componentWillReceiveProps', -> class NamedComponent extends React.Component @@ -503,28 +538,28 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(NamedComponent), 'SPAN', 'foo' - ).toErrorDev( + test React.createElement(NamedComponent), 'SPAN', 'foo' + assertConsoleErrorDev [ 'NamedComponent has a method called UNSAFE_componentWillRecieveProps(). - Did you mean UNSAFE_componentWillReceiveProps()?' - ) + Did you mean UNSAFE_componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)' + ] it 'should throw AND warn when trying to access classic APIs', -> ref = React.createRef() test React.createElement(InnerComponent, name: 'foo', ref: ref), 'DIV', 'foo' - expect(-> - expect(-> ref.current.replaceState {}).toThrow() - ).toWarnDev( - 'replaceState(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} - ) - expect(-> - expect(-> ref.current.isMounted()).toThrow() - ).toWarnDev( - 'isMounted(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} - ) + + expect(-> ref.current.replaceState {}).toThrow() + assertConsoleWarnDev([ + 'replaceState(...) is deprecated in plain JavaScript React classes. + Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).' + ], {withoutStack: true}) + + expect(-> ref.current.isMounted()).toThrow() + assertConsoleWarnDev([ + 'isMounted(...) is deprecated in plain JavaScript React classes. + Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks.', + ], {withoutStack: true}) if !featureFlags.disableLegacyContext it 'supports this.context passed via getChildContext', -> @@ -542,13 +577,25 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement Bar - expect(-> - test React.createElement(Foo), 'DIV', 'bar-through-context' - ).toErrorDev( - [ - 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - ], - ) + test React.createElement(Foo), 'DIV', 'bar-through-context' + if featureFlags.enableOwnerStacks + assertConsoleErrorDev [ + 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead. + (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)', + 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead. + (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)' + ] + else + assertConsoleErrorDev [ + 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead. + (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)', + 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead. + (https://react.dev/link/legacy-context)\n' + + ' in Bar (at **)\n' + + ' in Foo (at **)' + ] undefined diff --git a/packages/react/src/__tests__/ReactContextValidator-test.js b/packages/react/src/__tests__/ReactContextValidator-test.js index 359857af15..66597f0dd5 100644 --- a/packages/react/src/__tests__/ReactContextValidator-test.js +++ b/packages/react/src/__tests__/ReactContextValidator-test.js @@ -19,6 +19,7 @@ let PropTypes; let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; describe('ReactContextValidator', () => { beforeEach(() => { @@ -27,7 +28,7 @@ describe('ReactContextValidator', () => { PropTypes = require('prop-types'); React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); // TODO: This behavior creates a runtime dependency on propTypes. We should @@ -66,15 +67,20 @@ describe('ReactContextValidator', () => { let instance; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - (instance = current)} />, - ); - }); - }).toErrorDev([ - 'ComponentInFooBarContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + await act(() => { + root.render( + (instance = current)} />, + ); + }); + assertConsoleErrorDev([ + 'ComponentInFooBarContext uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentInFooBarContext (at **)', + 'Component uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in ComponentInFooBarContext (at **)' + : ' in Component (at **)'), ]); expect(instance.childRef.current.context).toEqual({foo: 'abc'}); }); @@ -144,13 +150,18 @@ describe('ReactContextValidator', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Parent (at **)', + 'Component uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in Parent (at **)' + : ' in Component (at **)'), ]); expect(constructorContext).toEqual({foo: 'abc'}); @@ -191,33 +202,33 @@ describe('ReactContextValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'ComponentA uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ComponentA.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on ComponentA or remove childContextTypes from it.', - ]); - - // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); + const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(); }); + assertConsoleErrorDev([ + 'ComponentA uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentA (at **)', + 'ComponentA.childContextTypes is specified but there is no getChildContext() method on the instance. ' + + 'You can either define getChildContext() on ComponentA or remove childContextTypes from it.\n' + + ' in ComponentA (at **)', + ]); - await expect(async () => { - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'ComponentB uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ComponentB.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on ComponentB or remove childContextTypes from it.', + // Warnings should be deduped by component type + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentB uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentB (at **)', + 'ComponentB.childContextTypes is specified but there is no getChildContext() method on the instance. ' + + 'You can either define getChildContext() on ComponentB or remove childContextTypes from it.\n' + + ' in ComponentB (at **)', ]); }); @@ -260,17 +271,34 @@ describe('ReactContextValidator', () => { foo: PropTypes.string.isRequired, }; - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'MiddleMissingContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'MiddleMissingContext.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on MiddleMissingContext or remove childContextTypes from it.', - 'ChildContextConsumer uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ParentContextProvider (at **)', + 'MiddleMissingContext uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in MiddleMissingContext (at **)\n') + + ' in ParentContextProvider (at **)', + 'MiddleMissingContext.childContextTypes is specified but there is no getChildContext() method on the instance. ' + + 'You can either define getChildContext() on MiddleMissingContext or remove childContextTypes from it.\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in MiddleMissingContext (at **)\n') + + ' in ParentContextProvider (at **)', + 'ChildContextConsumer uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in ChildContextConsumer (at **)\n') + + ' in MiddleMissingContext (at **)\n' + + ' in ParentContextProvider (at **)', ]); expect(childContext.bar).toBeUndefined(); expect(childContext.foo).toBe('FOO'); @@ -428,25 +456,7 @@ describe('ReactContextValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - }).toErrorDev([ - 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead', - 'ComponentA uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - 'ComponentA declares both contextTypes and contextType static properties. The legacy contextTypes property will be ignored.', - ]); - - // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); + const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render( @@ -455,19 +465,41 @@ describe('ReactContextValidator', () => { ); }); - await expect(async () => { - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - }).toErrorDev([ - 'ComponentB declares both contextTypes and contextType static properties. The legacy contextTypes property will be ignored.', - 'ComponentB uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + assertConsoleErrorDev([ + 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ParentContextProvider (at **)', + 'ComponentA declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.\n' + + ' in ComponentA (at **)', + 'ComponentA uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentA (at **)', + ]); + + // Warnings should be deduped by component type + await act(() => { + root.render( + + + , + ); + }); + + await act(() => { + root.render( + + + , + ); + }); + assertConsoleErrorDev([ + 'ComponentB declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.\n' + + ' in ComponentB (at **)', + 'ComponentB uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentB (at **)', ]); }); @@ -481,20 +513,17 @@ describe('ReactContextValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'ComponentA defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'Did you accidentally pass the Context.Consumer instead?', - ); + 'Did you accidentally pass the Context.Consumer instead?\n' + + ' in ComponentA (at **)', + ]); - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); @@ -505,8 +534,6 @@ describe('ReactContextValidator', () => { return
; } } - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); @@ -539,23 +566,22 @@ describe('ReactContextValidator', () => { } await expect(async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).rejects.toThrow( - "Cannot read properties of undefined (reading 'world')", - ); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + }).rejects.toThrow("Cannot read properties of undefined (reading 'world')"); + + assertConsoleErrorDev([ 'Foo defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + 'However, it is set to undefined. ' + 'This can be caused by a typo or by mixing up named and default imports. ' + 'This can also happen due to a circular dependency, ' + - 'so try moving the createContext() call to a separate file.', - ); + 'so try moving the createContext() call to a separate file.\n' + + ' in Foo (at **)', + ]); }); it('should warn when class contextType is an object', async () => { @@ -571,20 +597,19 @@ describe('ReactContextValidator', () => { } await expect(async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).rejects.toThrow( - "Cannot read properties of undefined (reading 'hello')", - ); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + }).rejects.toThrow("Cannot read properties of undefined (reading 'hello')"); + + assertConsoleErrorDev([ 'Foo defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'However, it is set to an object with keys {x, y}.', - ); + 'However, it is set to an object with keys {x, y}.\n' + + ' in Foo (at **)', + ]); }); it('should warn when class contextType is a primitive', async () => { @@ -596,20 +621,19 @@ describe('ReactContextValidator', () => { } await expect(async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).rejects.toThrow( - "Cannot read properties of undefined (reading 'world')", - ); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + }).rejects.toThrow("Cannot read properties of undefined (reading 'world')"); + + assertConsoleErrorDev([ 'Foo defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'However, it is set to a string.', - ); + 'However, it is set to a string.\n' + + ' in Foo (at **)', + ]); }); it('should warn if you define contextType on a function component', async () => { @@ -625,31 +649,26 @@ describe('ReactContextValidator', () => { } ComponentB.contextType = Context; - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'ComponentA: Function components do not support contextType.', - ); + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentA: Function components do not support contextType.\n' + + ' in ComponentA (at **)', + ]); // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); - await expect(async () => { - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'ComponentB: Function components do not support contextType.', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentB: Function components do not support contextType.\n' + + ' in ComponentB (at **)', + ]); }); }); diff --git a/packages/react/src/__tests__/ReactCreateElement-test.js b/packages/react/src/__tests__/ReactCreateElement-test.js index 392f89979e..44952536ac 100644 --- a/packages/react/src/__tests__/ReactCreateElement-test.js +++ b/packages/react/src/__tests__/ReactCreateElement-test.js @@ -13,6 +13,8 @@ let act; let React; let ReactDOMClient; +let assertConsoleErrorDev; +let assertConsoleWarnDev; // NOTE: This module tests the old, "classic" JSX runtime, React.createElement. // Do not use JSX syntax in this module; call React.createElement directly. @@ -22,7 +24,11 @@ describe('ReactCreateElement', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -63,25 +69,34 @@ describe('ReactCreateElement', () => { } } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(React.createElement(Parent)); - }); - }).toErrorDev( + await act(() => { + root.render(React.createElement(Parent)); + }); + assertConsoleErrorDev([ 'Child: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', - ); + 'prop. (https://react.dev/link/special-props)\n' + + (gate(flags => flags.enableOwnerStacks) + ? [' in Parent (at **)'] + : [ + ' in Child (at **)\n' + + ' in div (at **)\n' + + ' in Parent (at **)', + ]), + ]); }); it('should warn when `key` is being accessed on a host element', () => { const element = React.createElement('div', {key: '3'}); - expect(() => void element.props.key).toErrorDev( - 'div: `key` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', + void element.props.key; + assertConsoleErrorDev( + [ + 'div: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://react.dev/link/special-props)', + ], {withoutStack: true}, ); }); @@ -141,9 +156,16 @@ describe('ReactCreateElement', () => { foo: '56', }); expect(element.type).toBe(ComponentClass); - expect(() => expect(element.ref).toBe(ref)).toErrorDev( - 'Accessing element.ref was removed in React 19', - {withoutStack: true}, + expect(element.ref).toBe(ref); + assertConsoleErrorDev( + [ + 'Accessing element.ref was removed in React 19. ref is now a ' + + 'regular prop. It will be removed from the JSX Element ' + + 'type in a future release.', + ], + { + withoutStack: true, + }, ); const expectation = {foo: '56', ref}; Object.freeze(expectation); @@ -412,11 +434,13 @@ describe('ReactCreateElement', () => { it('warns if outdated JSX transform is detected', async () => { // Warns if __self is detected, because that's only passed by a compiler - expect(() => { - React.createElement('div', {className: 'foo', __self: this}); - }).toWarnDev( - 'Your app (or one of its dependencies) is using an outdated ' + - 'JSX transform.', + React.createElement('div', {className: 'foo', __self: this}); + assertConsoleWarnDev( + [ + 'Your app (or one of its dependencies) is using an outdated JSX ' + + 'transform. Update to the modern JSX transform for ' + + 'faster performance: https://react.dev/link/new-jsx-transform', + ], { withoutStack: true, }, diff --git a/packages/react/src/__tests__/ReactCreateRef-test.js b/packages/react/src/__tests__/ReactCreateRef-test.js index 616c62e00c..bf7166d8cf 100644 --- a/packages/react/src/__tests__/ReactCreateRef-test.js +++ b/packages/react/src/__tests__/ReactCreateRef-test.js @@ -12,6 +12,7 @@ let React; let ReactDOM; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactCreateRef', () => { beforeEach(() => { @@ -20,6 +21,7 @@ describe('ReactCreateRef', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); + ({assertConsoleErrorDev} = require('internal-test-utils')); }); it('should warn in dev if an invalid ref object is provided', () => { @@ -34,38 +36,36 @@ describe('ReactCreateRef', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - expect(() => - ReactDOM.flushSync(() => { - root.render( - -
- , - ); - }), - ).toErrorDev( + ReactDOM.flushSync(() => { + root.render( + +
+ , + ); + }); + assertConsoleErrorDev([ 'Unexpected ref object provided for div. ' + 'Use either a ref-setter function or React.createRef().\n' + ' in div (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in Wrapper (at **)'), - ); + ]); - expect(() => - ReactDOM.flushSync(() => { - root.render( - - - , - ); - }), - ).toErrorDev( + ReactDOM.flushSync(() => { + root.render( + + + , + ); + }); + assertConsoleErrorDev([ 'Unexpected ref object provided for ExampleComponent. ' + 'Use either a ref-setter function or React.createRef().\n' + ' in ExampleComponent (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in Wrapper (at **)'), - ); + ]); }); }); diff --git a/packages/react/src/__tests__/ReactES6Class-test.js b/packages/react/src/__tests__/ReactES6Class-test.js index eceeab18c5..640ae7ef37 100644 --- a/packages/react/src/__tests__/ReactES6Class-test.js +++ b/packages/react/src/__tests__/ReactES6Class-test.js @@ -14,6 +14,7 @@ let React; let ReactDOM; let ReactDOMClient; let assertConsoleErrorDev; +let assertConsoleWarnDev; describe('ReactES6Class', () => { let container; @@ -31,7 +32,10 @@ describe('ReactES6Class', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - ({assertConsoleErrorDev} = require('internal-test-utils')); + ({ + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); container = document.createElement('div'); root = ReactDOMClient.createRoot(container); attachedListener = null; @@ -69,14 +73,15 @@ describe('ReactES6Class', () => { } window.addEventListener('error', errorHandler); try { - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev([ + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ // A failed component renders twice in DEV in concurrent mode 'No `render` method found on the Foo instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Foo (at **)', 'No `render` method found on the Foo instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Foo (at **)', ]); } finally { window.removeEventListener('error', errorHandler); @@ -158,12 +163,12 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getDerivedStateFromError is not static', () => { @@ -175,12 +180,12 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromError() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getSnapshotBeforeUpdate is static', () => { @@ -190,12 +195,12 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.', - ); + 'and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)', + ]); }); it('warns if state not initialized before static getDerivedStateFromProps', () => { @@ -210,14 +215,14 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + 'undefined. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `Foo`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.', - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Foo (at **)', + ]); }); it('updates initial state with values returned by static getDerivedStateFromProps', () => { @@ -266,11 +271,13 @@ describe('ReactES6Class', () => { super(props, context); this.state = {tag: context.tag, className: this.context.className}; } + render() { const Tag = this.state.tag; return ; } } + Foo.contextTypes = { tag: PropTypes.string, className: PropTypes.string, @@ -280,10 +287,12 @@ describe('ReactES6Class', () => { getChildContext() { return {tag: 'span', className: 'foo'}; } + render() { return ; } } + Outer.childContextTypes = { tag: PropTypes.string, className: PropTypes.string, @@ -291,8 +300,15 @@ describe('ReactES6Class', () => { runTest(, 'SPAN', 'foo'); assertConsoleErrorDev([ - 'Outer uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Foo uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Outer uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + 'Foo uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in Foo (at **)\n') + + ' in Outer (at **)', ]); }); } @@ -327,9 +343,10 @@ describe('ReactES6Class', () => { return ; } } - expect(() => runTest(, 'SPAN', '')).toErrorDev( - 'Foo.state: must be set to an object or null', - ); + runTest(, 'SPAN', ''); + assertConsoleErrorDev([ + 'Foo.state: must be set to an object or null\n in Foo (at **)', + ]); }); }); @@ -480,11 +497,22 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev([ - 'getInitialState was defined on Foo, a plain JavaScript class.', - 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'contextType was defined as an instance property on Foo.', - 'contextTypes was defined as an instance property on Foo.', + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'getInitialState was defined on Foo, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?\n' + + ' in Foo (at **)', + 'getDefaultProps was defined on Foo, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Use a static property to define defaultProps instead.\n' + + ' in Foo (at **)', + 'contextType was defined as an instance property on Foo. ' + + 'Use a static property to define contextType instead.\n' + + ' in Foo (at **)', + 'contextTypes was defined as an instance property on Foo. ' + + 'Use a static property to define contextTypes instead.\n' + + ' in Foo (at **)', ]); expect(getInitialStateWasCalled).toBe(false); expect(getDefaultPropsWasCalled).toBe(false); @@ -514,11 +542,13 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev( + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ 'NamedComponent has a method called componentShouldUpdate(). Did you ' + 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.', - ); + 'because the function is expected to return a value.\n' + + ' in NamedComponent (at **)', + ]); }); it('should warn when misspelling componentWillReceiveProps', () => { @@ -531,10 +561,12 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev( + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ 'NamedComponent has a method called componentWillRecieveProps(). Did ' + - 'you mean componentWillReceiveProps()?', - ); + 'you mean componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)', + ]); }); it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => { @@ -547,23 +579,33 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev( + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ 'NamedComponent has a method called UNSAFE_componentWillRecieveProps(). ' + - 'Did you mean UNSAFE_componentWillReceiveProps()?', - ); + 'Did you mean UNSAFE_componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)', + ]); }); it('should throw AND warn when trying to access classic APIs', () => { const ref = React.createRef(); runTest(, 'DIV', 'foo'); - expect(() => - expect(() => ref.current.replaceState({})).toThrow(), - ).toWarnDev( - 'replaceState(...) is deprecated in plain JavaScript React classes', + + expect(() => ref.current.replaceState({})).toThrow(); + assertConsoleWarnDev( + [ + 'replaceState(...) is deprecated in plain JavaScript React classes. ' + + 'Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).', + ], {withoutStack: true}, ); - expect(() => expect(() => ref.current.isMounted()).toThrow()).toWarnDev( - 'isMounted(...) is deprecated in plain JavaScript React classes', + expect(() => ref.current.isMounted()).toThrow(); + assertConsoleWarnDev( + [ + 'isMounted(...) is deprecated in plain JavaScript React classes. ' + + 'Instead, make sure to clean up subscriptions and pending requests in ' + + 'componentWillUnmount to prevent memory leaks.', + ], {withoutStack: true}, ); }); @@ -575,20 +617,31 @@ describe('ReactES6Class', () => { return
; } } + Bar.contextTypes = {bar: PropTypes.string}; + class Foo extends React.Component { getChildContext() { return {bar: 'bar-through-context'}; } + render() { return ; } } + Foo.childContextTypes = {bar: PropTypes.string}; runTest(, 'DIV', 'bar-through-context'); assertConsoleErrorDev([ - 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Foo uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)', + 'Bar uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in Bar (at **)\n') + + ' in Foo (at **)', ]); }); } diff --git a/packages/react/src/__tests__/ReactElementClone-test.js b/packages/react/src/__tests__/ReactElementClone-test.js index fb0bfe2df8..b2e791f826 100644 --- a/packages/react/src/__tests__/ReactElementClone-test.js +++ b/packages/react/src/__tests__/ReactElementClone-test.js @@ -12,6 +12,7 @@ let act; let React; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactElementClone', () => { let ComponentClass; @@ -19,7 +20,7 @@ describe('ReactElementClone', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -314,11 +315,14 @@ describe('ReactElementClone', () => { it('warns for keys for arrays of elements in rest args', async () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(React.cloneElement(
, null, [
,
])); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + await act(() => { + root.render(React.cloneElement(
, null, [
,
])); + }); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); }); it('does not warns for arrays of elements with keys', async () => { @@ -363,9 +367,16 @@ describe('ReactElementClone', () => { expect(clone.type).toBe(ComponentClass); expect(clone.key).toBe('12'); expect(clone.props.ref).toBe('34'); - expect(() => expect(clone.ref).toBe('34')).toErrorDev( - 'Accessing element.ref was removed in React 19', - {withoutStack: true}, + expect(clone.ref).toBe('34'); + assertConsoleErrorDev( + [ + 'Accessing element.ref was removed in React 19. ref is now a ' + + 'regular prop. It will be removed from the JSX Element ' + + 'type in a future release.', + ], + { + withoutStack: true, + }, ); expect(clone.props).toEqual({foo: 'ef', ref: '34'}); if (__DEV__) { diff --git a/packages/react/src/__tests__/ReactElementValidator-test.internal.js b/packages/react/src/__tests__/ReactElementValidator-test.internal.js index 4383a6472d..1191aa7ec2 100644 --- a/packages/react/src/__tests__/ReactElementValidator-test.internal.js +++ b/packages/react/src/__tests__/ReactElementValidator-test.internal.js @@ -18,6 +18,7 @@ let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; describe('ReactElementValidator', () => { let ComponentClass; @@ -27,7 +28,7 @@ describe('ReactElementValidator', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); ComponentClass = class extends React.Component { render() { return React.createElement('div', null, this.props.children); @@ -37,16 +38,27 @@ describe('ReactElementValidator', () => { it('warns for keys for arrays of elements in rest args', async () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => - root.render( - React.createElement(ComponentClass, null, [ - React.createElement(ComponentClass), - React.createElement(ComponentClass), - ]), - ), - ); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + await act(() => + root.render( + React.createElement(ComponentClass, null, [ + React.createElement(ComponentClass), + React.createElement(ComponentClass), + ]), + ), + ); + assertConsoleErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + ] + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + ], + ); }); it('warns for keys for arrays of elements with owner info', async () => { @@ -67,18 +79,23 @@ describe('ReactElementValidator', () => { } } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(ComponentWrapper))); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(ComponentWrapper))); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `' + (gate(flags => flags.enableOwnerStacks) ? 'ComponentClass' : 'InnerClass') + '`. ' + - 'It was passed a child from ComponentWrapper. ', - ); + 'It was passed a child from ComponentWrapper. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in ComponentWrapper (at **)' + : ' in ComponentClass (at **)\n' + + ' in InnerClass (at **)\n' + + ' in ComponentWrapper (at **)'), + ]); }); it('warns for keys for arrays with no owner or parent info', async () => { @@ -89,36 +106,35 @@ describe('ReactElementValidator', () => { const divs = [
,
]; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render({divs})); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render({divs})); + assertConsoleErrorDev([ gate(flags => flags.enableOwnerStacks) ? // For owner stacks the parent being validated is the div. 'Each child in a list should have a unique ' + - '"key" prop.' + - '\n\nCheck the top-level render call using
. ' + - 'See https://react.dev/link/warning-keys for more information.\n' + - ' in div (at **)' + '"key" prop.' + + '\n\nCheck the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)' : 'Each child in a list should have a unique ' + - '"key" prop. See https://react.dev/link/warning-keys for more information.\n' + - ' in div (at **)', - ); + '"key" prop. See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); }); it('warns for keys for arrays of elements with no owner info', async () => { const divs = [
,
]; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); + const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(
{divs}
)); - }).toErrorDev( + await act(() => root.render(
{divs}
)); + assertConsoleErrorDev([ 'Each child in a list should have a unique ' + - '"key" prop.\n\nCheck the top-level render call using
. See ' + - 'https://react.dev/link/warning-keys for more information.\n' + + '"key" prop.' + + '\n\nCheck the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + ' in div (at **)', - ); + ]); }); it('warns for keys with component stack info', async () => { @@ -134,10 +150,9 @@ describe('ReactElementValidator', () => { return } />; } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render()); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render()); + assertConsoleErrorDev([ 'Each child in a list should have a unique ' + '"key" prop.\n\nCheck the render method of `Component`. See ' + 'https://react.dev/link/warning-keys for more information.\n' + @@ -147,7 +162,7 @@ describe('ReactElementValidator', () => { ? '' : ' in Parent (at **)\n') + ' in GrandParent (at **)', - ); + ]); }); it('does not warn for keys when passing children down', async () => { @@ -187,21 +202,37 @@ describe('ReactElementValidator', () => { }, }; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => - root.render(React.createElement(ComponentClass, null, iterable)), - ); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => + root.render(React.createElement(ComponentClass, null, iterable)), + ); + assertConsoleErrorDev( gate(flag => flag.enableOwnerStacks) - ? 'Each child in a list should have a unique "key" prop.' + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + ] : // Since each pass generates a new element, it doesn't get marked as // validated and it gets rechecked each time. - [ - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)\n' + + ' in ComponentClass (at **)', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)\n' + + ' in div (at **)\n' + + ' in ComponentClass (at **)', ], ); }); @@ -254,86 +285,102 @@ describe('ReactElementValidator', () => { function ParentComp() { return React.createElement(MyComp); } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(ParentComp))); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(ParentComp))); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in div (at **)\n' + ' in MyComp (at **)\n' + ' in ParentComp (at **)', - ); + ]); }); it('gives a helpful error when passing invalid types', async () => { function Foo() {} const errors = []; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div'), { - onUncaughtError(error) { - errors.push(error.message); - }, - }); - const cases = [ - React.createElement(undefined), - React.createElement(null), - React.createElement(true), - React.createElement({x: 17}), - React.createElement({}), - React.createElement(React.createElement('div')), - React.createElement(React.createElement(Foo)), - React.createElement( - React.createElement(React.createContext().Consumer), - ), - React.createElement({$$typeof: 'non-react-thing'}), - ]; - for (let i = 0; i < cases.length; i++) { - await act(() => root.render(cases[i])); - } - }).toErrorDev( - gate(flag => flag.enableOwnerStacks) - ? // We don't need these extra warnings because we already have the errors. - [] - : [ - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: undefined. You likely forgot to export your ' + - "component from the file it's defined in, or you might have mixed up " + - 'default and named imports.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: null.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: boolean.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: object.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: object. You likely forgot to export your ' + - "component from the file it's defined in, or you might have mixed up " + - 'default and named imports.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got:
. Did you accidentally export a JSX literal ' + - 'instead of a component?', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: . Did you accidentally export a JSX literal ' + - 'instead of a component?', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: . Did you accidentally ' + - 'export a JSX literal instead of a component?', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: object.', - ], - {withoutStack: true}, - ); + const root = ReactDOMClient.createRoot(document.createElement('div'), { + onUncaughtError(error) { + errors.push(error.message); + }, + }); + const cases = [ + [ + () => React.createElement(undefined), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: undefined. You likely forgot to export your ' + + "component from the file it's defined in, or you might have mixed up " + + 'default and named imports.', + ], + [ + () => React.createElement(null), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: null.', + ], + [ + () => React.createElement(true), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: boolean.', + ], + [ + () => React.createElement({x: 17}), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object.', + ], + [ + () => React.createElement({}), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object. You likely forgot to export your ' + + "component from the file it's defined in, or you might have mixed up " + + 'default and named imports.', + ], + [ + () => React.createElement(React.createElement('div')), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got:
. Did you accidentally export a JSX literal ' + + 'instead of a component?', + ], + [ + () => React.createElement(React.createElement(Foo)), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: . Did you accidentally export a JSX literal ' + + 'instead of a component?', + ], + [ + () => + React.createElement( + React.createElement(React.createContext().Consumer), + ), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: . Did you accidentally ' + + 'export a JSX literal instead of a component?', + ], + [ + () => React.createElement({$$typeof: 'non-react-thing'}), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object.', + ], + ]; + for (let i = 0; i < cases.length; i++) { + await act(async () => root.render(cases[i][0]())); + assertConsoleErrorDev( + gate(flag => flag.enableOwnerStacks) + ? // We don't need these extra warnings because we already have the errors. + [] + : [cases[i][1]], + {withoutStack: true}, + ); + } expect(errors).toEqual( __DEV__ @@ -414,15 +461,14 @@ describe('ReactElementValidator', () => { } await expect(async () => { - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(ParentComp))); - }).rejects.toThrowError( - 'Element type is invalid: expected a string (for built-in components) ' + - 'or a class/function (for composite components) but got: null.' + - (__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''), - ); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(ParentComp))); + }).rejects.toThrowError( + 'Element type is invalid: expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: null.' + + (__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''), + ); + assertConsoleErrorDev( gate(flag => flag.enableOwnerStacks) ? // We don't need these extra warnings because we already have the errors. [] @@ -446,13 +492,13 @@ describe('ReactElementValidator', () => { } } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(Foo))); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(Foo))); + assertConsoleErrorDev([ 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + - 'can only have `key` and `children` props.', - ); + 'can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ]); }); it('does not warn when using DOM node as children', async () => { @@ -512,9 +558,8 @@ describe('ReactElementValidator', () => { it('does not blow up on key warning with undefined type', () => { const Foo = undefined; - expect(() => { - void ({[
]}); - }).toErrorDev( + void ({[
]}); + assertConsoleErrorDev( gate(flags => flags.enableOwnerStacks) ? [] : [ diff --git a/packages/react/src/__tests__/ReactJSXElementValidator-test.js b/packages/react/src/__tests__/ReactJSXElementValidator-test.js index 12e54e5c96..d4e78f1b1c 100644 --- a/packages/react/src/__tests__/ReactJSXElementValidator-test.js +++ b/packages/react/src/__tests__/ReactJSXElementValidator-test.js @@ -14,6 +14,7 @@ let act; let React; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactJSXElementValidator', () => { let Component; @@ -22,7 +23,7 @@ describe('ReactJSXElementValidator', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -41,14 +42,21 @@ describe('ReactJSXElementValidator', () => { }); it('warns for keys for arrays of elements in children position', async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render({[, ]}); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + await act(() => { + root.render({[, ]}); + }); + assertConsoleErrorDev([ + gate(flags => flags.enableOwnerStacks) + ? 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)' + : 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)', + ]); }); it('warns for keys for arrays of elements with owner info', async () => { @@ -64,20 +72,24 @@ describe('ReactJSXElementValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `' + (gate(flag => flag.enableOwnerStacks) ? 'Component' : 'InnerComponent') + '`. ' + - 'It was passed a child from ComponentWrapper. ', + 'It was passed a child from ComponentWrapper. See https://react.dev/link/warning-keys for more information.\n' + + (gate(flag => flag.enableOwnerStacks) + ? ' in ComponentWrapper (at **)' + : ' in Component (at **)\n' + + ' in InnerComponent (at **)\n' + + ' in ComponentWrapper (at **)'), ]); }); @@ -94,22 +106,38 @@ describe('ReactJSXElementValidator', () => { }, }; - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render({iterable}); - }); - }).toErrorDev( + await act(() => { + root.render({iterable}); + }); + assertConsoleErrorDev( gate(flag => flag.enableOwnerStacks) - ? ['Each child in a list should have a unique "key" prop.'] + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)', + ] : // Since each pass generates a new element, it doesn't get marked as // validated and it gets rechecked each time. [ - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)\n' + + ' in Component (at **)', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)\n' + + ' in div (at **)\n' + + ' in Component (at **)', ], ); }); @@ -198,21 +226,20 @@ describe('ReactJSXElementValidator', () => { return ; } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in div (at **)\n' + ' in MyComp (at **)\n' + ' in ParentComp (at **)', - ); + ]); }); it('warns for fragments with illegal attributes', async () => { @@ -222,16 +249,16 @@ describe('ReactJSXElementValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + - 'can only have `key` and `children` props.', - ); + 'can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ]); }); it('warns for fragments with refs', async () => { @@ -248,13 +275,16 @@ describe('ReactJSXElementValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev('Invalid prop `ref` supplied to `React.Fragment`.'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Invalid prop `ref` supplied to `React.Fragment`.' + + ' React.Fragment can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ]); }); it('does not warn for fragments of multiple elements without keys', async () => { @@ -271,19 +301,24 @@ describe('ReactJSXElementValidator', () => { }); it('warns for fragments of multiple elements with same key', async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - <> - 1 - 2 - 3 - , - ); - }); - }).toErrorDev('Encountered two children with the same key, `a`.'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + <> + 1 + 2 + 3 + , + ); + }); + assertConsoleErrorDev([ + 'Encountered two children with the same key, `a`. ' + + 'Keys should be unique so that components maintain their identity across updates. ' + + 'Non-unique keys may cause children to be duplicated and/or omitted — ' + + 'the behavior is unsupported and could change in a future version.\n' + + ' in span (at **)', + ]); }); it('does not call lazy initializers eagerly', () => { diff --git a/packages/react/src/__tests__/ReactJSXRuntime-test.js b/packages/react/src/__tests__/ReactJSXRuntime-test.js index e3de4dbf5e..663c935caf 100644 --- a/packages/react/src/__tests__/ReactJSXRuntime-test.js +++ b/packages/react/src/__tests__/ReactJSXRuntime-test.js @@ -14,6 +14,7 @@ let ReactDOMClient; let JSXRuntime; let JSXDEVRuntime; let act; +let assertConsoleErrorDev; // NOTE: Prefer to call the JSXRuntime directly in these tests so we can be // certain that we are testing the runtime behavior, as opposed to the Babel @@ -26,7 +27,7 @@ describe('ReactJSXRuntime', () => { JSXRuntime = require('react/jsx-runtime'); JSXDEVRuntime = require('react/jsx-dev-runtime'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('allows static methods to be called using the type property', () => { @@ -205,41 +206,49 @@ describe('ReactJSXRuntime', () => { }); } } - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsx(Parent, {})); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsx(Parent, {})); + }); + assertConsoleErrorDev([ 'Child: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', - ); + 'prop. (https://react.dev/link/special-props)\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in Parent (at **)' + : ' in Child (at **)\n' + + ' in div (at **)\n' + + ' in Parent (at **)'), + ]); }); it('warns when a jsxs is passed something that is not an array', async () => { const container = document.createElement('div'); - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsxs('div', {children: 'foo'}, null)); - }); - }).toErrorDev( - 'React.jsx: Static children should always be an array. ' + - 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + - 'Use the Babel transform instead.', + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsxs('div', {children: 'foo'}, null)); + }); + assertConsoleErrorDev( + [ + 'React.jsx: Static children should always be an array. ' + + 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + + 'Use the Babel transform instead.', + ], {withoutStack: true}, ); }); it('should warn when `key` is being accessed on a host element', () => { const element = JSXRuntime.jsxs('div', {}, '3'); - expect(() => void element.props.key).toErrorDev( - 'div: `key` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', + void element.props.key; + assertConsoleErrorDev( + [ + 'div: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://react.dev/link/special-props)', + ], {withoutStack: true}, ); }); @@ -263,19 +272,18 @@ describe('ReactJSXRuntime', () => { }); } } - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsx(Parent, {})); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsx(Parent, {})); + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.\n\n' + 'Check the render method of `Parent`. See https://react.dev/link/warning-keys for more information.\n' + (gate(flags => flags.enableOwnerStacks) ? '' : ' in Child (at **)\n') + ' in Parent (at **)', - ); + ]); }); it('should warn when keys are passed as part of props', async () => { @@ -292,19 +300,19 @@ describe('ReactJSXRuntime', () => { }); } } - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsx(Parent, {})); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsx(Parent, {})); + }); + assertConsoleErrorDev([ 'A props object containing a "key" prop is being spread into JSX:\n' + ' let props = {key: someKey, prop: ...};\n' + ' \n' + 'React keys must be passed directly to JSX without using spread:\n' + ' let props = {prop: ...};\n' + - ' ', - ); + ' \n' + + ' in Parent (at **)', + ]); }); it('should not warn when unkeyed children are passed to jsxs', async () => { @@ -368,13 +376,18 @@ describe('ReactJSXRuntime', () => { key: 'key', }; - let elementWithSpreadKey; - expect(() => { - elementWithSpreadKey = __DEV__ - ? JSXDEVRuntime.jsxDEV('div', configWithKey) - : JSXRuntime.jsx('div', configWithKey); - }).toErrorDev( - 'A props object containing a "key" prop is being spread into JSX', + const elementWithSpreadKey = __DEV__ + ? JSXDEVRuntime.jsxDEV('div', configWithKey) + : JSXRuntime.jsx('div', configWithKey); + assertConsoleErrorDev( + [ + 'A props object containing a "key" prop is being spread into JSX:\n' + + ' let props = {key: someKey, foo: ..., bar: ...};\n' + + '
\n' + + 'React keys must be passed directly to JSX without using spread:\n' + + ' let props = {foo: ..., bar: ...};\n' + + '
', + ], {withoutStack: true}, ); expect(elementWithSpreadKey.props).not.toBe(configWithKey); diff --git a/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js b/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js index f0caf6b494..5075c4a9a0 100644 --- a/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js +++ b/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js @@ -12,6 +12,7 @@ let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; // TODO: Historically this module was used to confirm that the JSX transform // produces the correct output. However, most users (and indeed our own test @@ -29,7 +30,7 @@ describe('ReactJSXTransformIntegration', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); Component = class extends React.Component { render() { @@ -112,8 +113,13 @@ describe('ReactJSXTransformIntegration', () => { const ref = React.createRef(); const element = ; expect(element.type).toBe(Component); - expect(() => expect(element.ref).toBe(ref)).toErrorDev( - 'Accessing element.ref was removed in React 19', + expect(element.ref).toBe(ref); + assertConsoleErrorDev( + [ + 'Accessing element.ref was removed in React 19. ref is now a ' + + 'regular prop. It will be removed from the JSX Element ' + + 'type in a future release.', + ], {withoutStack: true}, ); const expectation = {foo: '56', ref}; diff --git a/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js b/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js index d8fc623a3d..20395118c5 100644 --- a/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js +++ b/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js @@ -14,6 +14,7 @@ let ReactDOMClient; let ReactFeatureFlags; let act; let container; +let assertConsoleErrorDev; function loadModules({ enableProfilerTimer = true, @@ -31,6 +32,7 @@ function loadModules({ ReactDOMClient = require('react-dom/client'); const InternalTestUtils = require('internal-test-utils'); act = InternalTestUtils.act; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; } describe('Profiler', () => { @@ -54,12 +56,13 @@ describe('Profiler', () => { if (__DEV__ && enableProfilerTimer) { it('should warn if required params are missing', async () => { const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Profiler must specify an "id" of type `string` as a prop. Received the type `undefined` instead.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + [ + 'Profiler must specify an "id" of type `string` as a prop. Received the type `undefined` instead.', + ], { withoutStack: true, }, diff --git a/packages/react/src/__tests__/ReactPureComponent-test.js b/packages/react/src/__tests__/ReactPureComponent-test.js index 9efdf183ee..c54945ff25 100644 --- a/packages/react/src/__tests__/ReactPureComponent-test.js +++ b/packages/react/src/__tests__/ReactPureComponent-test.js @@ -10,13 +10,13 @@ 'use strict'; let act; - +let assertConsoleErrorDev; let React; let ReactDOMClient; describe('ReactPureComponent', () => { beforeEach(() => { - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -90,16 +90,15 @@ describe('ReactPureComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '' + - 'Component has a method called shouldComponentUpdate(). ' + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Component has a method called shouldComponentUpdate(). ' + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + - 'Please extend React.Component if shouldComponentUpdate is used.', - ); + 'Please extend React.Component if shouldComponentUpdate is used.\n' + + ' in Component (at **)', + ]); await act(() => { root.render(); }); @@ -133,15 +132,14 @@ describe('ReactPureComponent', () => { } } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '' + - 'PureComponent has a method called shouldComponentUpdate(). ' + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'PureComponent has a method called shouldComponentUpdate(). ' + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + - 'Please extend React.Component if shouldComponentUpdate is used.', - ); + 'Please extend React.Component if shouldComponentUpdate is used.\n' + + ' in PureComponent (at **)', + ]); }); }); diff --git a/packages/react/src/__tests__/ReactStartTransition-test.js b/packages/react/src/__tests__/ReactStartTransition-test.js index 9e689ac6e7..00387dbb26 100644 --- a/packages/react/src/__tests__/ReactStartTransition-test.js +++ b/packages/react/src/__tests__/ReactStartTransition-test.js @@ -12,6 +12,7 @@ let React; let ReactTestRenderer; let act; +let assertConsoleWarnDev; let useState; let useTransition; @@ -22,7 +23,7 @@ describe('ReactStartTransition', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - act = require('internal-test-utils').act; + ({act, assertConsoleWarnDev} = require('internal-test-utils')); useState = React.useState; useTransition = React.useTransition; }); @@ -53,15 +54,14 @@ describe('ReactStartTransition', () => { }); }); - await expect(async () => { - await act(() => { - React.startTransition(() => { - subs.forEach(setState => { - setState(state => state + 1); - }); + await act(() => { + React.startTransition(() => { + subs.forEach(setState => { + setState(state => state + 1); }); }); - }).toWarnDev( + }); + assertConsoleWarnDev( [ 'Detected a large number of updates inside startTransition. ' + 'If this is due to a subscription please re-write it to use React provided hooks. ' + @@ -70,15 +70,14 @@ describe('ReactStartTransition', () => { {withoutStack: true}, ); - await expect(async () => { - await act(() => { - triggerHookTransition(() => { - subs.forEach(setState => { - setState(state => state + 1); - }); + await act(() => { + triggerHookTransition(() => { + subs.forEach(setState => { + setState(state => state + 1); }); }); - }).toWarnDev( + }); + assertConsoleWarnDev( [ 'Detected a large number of updates inside startTransition. ' + 'If this is due to a subscription please re-write it to use React provided hooks. ' + diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index f28c70a871..6d44a28881 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -19,6 +19,7 @@ let useMemo; let useState; let useReducer; let assertConsoleErrorDev; +let assertConsoleWarnDev; describe('ReactStrictMode', () => { beforeEach(() => { @@ -27,7 +28,11 @@ describe('ReactStrictMode', () => { ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); - ({act, assertConsoleErrorDev} = require('internal-test-utils')); + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); useMemo = React.useMemo; useState = React.useState; useReducer = React.useReducer; @@ -40,20 +45,19 @@ describe('ReactStrictMode', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - - - , - ); - }); - }).toErrorDev( + await act(() => { + root.render( + + + , + ); + }); + assertConsoleErrorDev([ 'Invalid ARIA attribute `ariaTypo`. ' + 'ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in div (at **)\n' + ' in Foo (at **)', - ); + ]); }); it('should appear in the SSR component stack', () => { @@ -61,18 +65,17 @@ describe('ReactStrictMode', () => { return
; } - expect(() => { - ReactDOMServer.renderToString( - - - , - ); - }).toErrorDev( + ReactDOMServer.renderToString( + + + , + ); + assertConsoleErrorDev([ 'Invalid ARIA attribute `ariaTypo`. ' + 'ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in div (at **)\n' + ' in Foo (at **)', - ); + ]); }); // @gate __DEV__ @@ -620,9 +623,8 @@ describe('Concurrent Mode', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect( - async () => await act(() => root.render()), - ).toErrorDev( + await act(() => root.render()); + assertConsoleErrorDev( [ `Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. @@ -681,31 +683,29 @@ Please update the following components: App`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await expect( - async () => await act(() => root.render()), - ).toErrorDev( - [ - `Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. + await act(() => root.render()); + assertConsoleErrorDev( + [ + `Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. * Move code with side effects to componentDidMount, and set initial state in the constructor. Please update the following components: App`, - `Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. + `Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. * Move data fetching code or side effects to componentDidUpdate. * If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state Please update the following components: Child`, - `Using UNSAFE_componentWillUpdate in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. + `Using UNSAFE_componentWillUpdate in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. * Move data fetching code or side effects to componentDidUpdate. Please update the following components: App`, - ], - {withoutStack: true}, - ); - }).toWarnDev( + ], + {withoutStack: true}, + ); + assertConsoleWarnDev( [ `componentWillMount has been renamed, and is not recommended for use. See https://react.dev/link/unsafe-component-lifecycles for details. @@ -752,17 +752,25 @@ Please update the following components: Parent`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => root.render()); - }).toErrorDev( - 'Using UNSAFE_componentWillMount in strict mode is not recommended', + await act(() => root.render()); + assertConsoleErrorDev( + [ + 'Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n\n' + + 'Please update the following components: Foo', + ], {withoutStack: true}, ); - await expect(async () => { - await act(() => root.render()); - }).toErrorDev( - 'Using UNSAFE_componentWillMount in strict mode is not recommended', + await act(() => root.render()); + assertConsoleErrorDev( + [ + 'Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n\n' + + 'Please update the following components: Bar', + ], {withoutStack: true}, ); @@ -810,12 +818,20 @@ Please update the following components: Parent`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Using UNSAFE_componentWillReceiveProps in strict mode is not recommended', + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + [ + 'Using UNSAFE_componentWillReceiveProps in strict mode is not recommended ' + + 'and may indicate bugs in your code. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, " + + 'refactor your code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n\n' + + 'Please update the following components: Bar, Foo', + ], {withoutStack: true}, ); diff --git a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts index 00e3a8b3ff..5f51cc6f38 100644 --- a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts +++ b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts @@ -16,9 +16,11 @@ import ReactDOM = require('react-dom'); import ReactDOMClient = require('react-dom/client'); import PropTypes = require('prop-types'); import ReactFeatureFlags = require('shared/ReactFeatureFlags'); +import TestUtils = require('internal-test-utils'); // Before Each - +const assertConsoleErrorDev = TestUtils.assertConsoleErrorDev; +const assertConsoleWarnDev = TestUtils.assertConsoleWarnDev; let container; let root; let attachedListener = null; @@ -313,19 +315,19 @@ class ClassicRefs extends React.Component { // Describe the actual test cases. -describe('ReactTypeScriptClass', function() { - beforeEach(function() { +describe('ReactTypeScriptClass', function () { + beforeEach(function () { container = document.createElement('div'); root = ReactDOMClient.createRoot(container); attachedListener = null; renderedName = null; }); - it('preserves the name of the class for use in error messages', function() { + it('preserves the name of the class for use in error messages', function () { expect(Empty.name).toBe('Empty'); }); - it('throws if no render function is defined', function() { + it('throws if no render function is defined', function () { class Foo extends React.Component {} const caughtErrors = []; function errorHandler(event) { @@ -334,14 +336,15 @@ describe('ReactTypeScriptClass', function() { } window.addEventListener('error', errorHandler); try { - expect(() => { - ReactDOM.flushSync(() => root.render(React.createElement(Empty))) - }).toErrorDev([ + ReactDOM.flushSync(() => root.render(React.createElement(Empty))); + assertConsoleErrorDev([ // A failed component renders twice in DEV in concurrent mode 'No `render` method found on the Empty instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Empty (at **)', 'No `render` method found on the Empty instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Empty (at **)', ]); } finally { window.removeEventListener('error', errorHandler); @@ -349,31 +352,31 @@ describe('ReactTypeScriptClass', function() { expect(caughtErrors.length).toBe(1); }); - it('renders a simple stateless component with prop', function() { + it('renders a simple stateless component with prop', function () { test(React.createElement(SimpleStateless, {bar: 'foo'}), 'DIV', 'foo'); test(React.createElement(SimpleStateless, {bar: 'bar'}), 'DIV', 'bar'); }); - it('renders based on state using initial values in this.props', function() { + it('renders based on state using initial values in this.props', function () { test( React.createElement(InitialState, {initialValue: 'foo'}), 'SPAN', - 'foo' + 'foo', ); }); - it('renders based on state using props in the constructor', function() { + it('renders based on state using props in the constructor', function () { const ref = React.createRef(); test( React.createElement(StateBasedOnProps, {initialValue: 'foo', ref: ref}), 'DIV', - 'foo' + 'foo', ); ReactDOM.flushSync(() => ref.current.changeState()); test(React.createElement(StateBasedOnProps), 'SPAN', 'bar'); }); - it('sets initial state with value returned by static getDerivedStateFromProps', function() { + it('sets initial state with value returned by static getDerivedStateFromProps', function () { class Foo extends React.Component { state = { foo: null, @@ -394,7 +397,7 @@ describe('ReactTypeScriptClass', function() { test(React.createElement(Foo, {foo: 'foo'}), 'DIV', 'foo bar'); }); - it('warns if getDerivedStateFromProps is not static', function() { + it('warns if getDerivedStateFromProps is not static', function () { class Foo extends React.Component { getDerivedStateFromProps() { return {}; @@ -403,17 +406,17 @@ describe('ReactTypeScriptClass', function() { return React.createElement('div', {}); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( - 'Foo: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), ); + assertConsoleErrorDev([ + 'Foo: getDerivedStateFromProps() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); - it('warns if getDerivedStateFromError is not static', function() { + it('warns if getDerivedStateFromError is not static', function () { class Foo extends React.Component { getDerivedStateFromError() { return {}; @@ -422,34 +425,34 @@ describe('ReactTypeScriptClass', function() { return React.createElement('div'); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( - 'Foo: getDerivedStateFromError() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), ); + assertConsoleErrorDev([ + 'Foo: getDerivedStateFromError() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); - it('warns if getSnapshotBeforeUpdate is static', function() { + it('warns if getSnapshotBeforeUpdate is static', function () { class Foo extends React.Component { static getSnapshotBeforeUpdate() {} render() { return React.createElement('div', {}); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( - 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.' + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), ); + assertConsoleErrorDev([ + 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + + 'and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)', + ]); }); - it('warns if state not initialized before static getDerivedStateFromProps', function() { + it('warns if state not initialized before static getDerivedStateFromProps', function () { class Foo extends React.Component { static getDerivedStateFromProps(nextProps, prevState) { return { @@ -463,19 +466,19 @@ describe('ReactTypeScriptClass', function() { }); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), + ); + assertConsoleErrorDev([ '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + 'undefined. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `Foo`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.' - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Foo (at **)', + ]); }); - it('updates initial state with values returned by static getDerivedStateFromProps', function() { + it('updates initial state with values returned by static getDerivedStateFromProps', function () { class Foo extends React.Component { state = { foo: 'foo', @@ -495,7 +498,7 @@ describe('ReactTypeScriptClass', function() { test(React.createElement(Foo), 'DIV', 'not-foo bar'); }); - it('renders updated state with values returned by static getDerivedStateFromProps', function() { + it('renders updated state with values returned by static getDerivedStateFromProps', function () { class Foo extends React.Component { state = { value: 'initial', @@ -517,66 +520,80 @@ describe('ReactTypeScriptClass', function() { }); if (!ReactFeatureFlags.disableLegacyContext) { - it('renders based on context in the constructor', function() { - expect(() => test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo')).toErrorDev([ - 'ProvideChildContextTypes uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'StateBasedOnContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.' + it('renders based on context in the constructor', function () { + test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'ProvideChildContextTypes uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ProvideChildContextTypes (at **)', + 'StateBasedOnContext uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (ReactFeatureFlags.enableOwnerStacks + ? ' in ProvideChildContextTypes.Object..ProvideChildContextTypes (at **)' + : ' in StateBasedOnContext (at **)\n') + + ' in ProvideChildContextTypes (at **)', ]); }); } - it('renders only once when setting state in componentWillMount', function() { + it('renders only once when setting state in componentWillMount', function () { renderCount = 0; test(React.createElement(RenderOnce, {initialValue: 'foo'}), 'SPAN', 'bar'); expect(renderCount).toBe(1); }); - it('should warn with non-object in the initial state property', function() { - expect(() => test(React.createElement(ArrayState), 'SPAN', '')).toErrorDev( - 'ArrayState.state: must be set to an object or null' - ); - expect(() => test(React.createElement(StringState), 'SPAN', '')).toErrorDev( - 'StringState.state: must be set to an object or null' - ); - expect(() => test(React.createElement(NumberState), 'SPAN', '')).toErrorDev( - 'NumberState.state: must be set to an object or null' - ); + it('should warn with non-object in the initial state property', function () { + test(React.createElement(ArrayState), 'SPAN', ''); + assertConsoleErrorDev([ + 'ArrayState.state: must be set to an object or null\n' + + ' in ArrayState (at **)', + ]); + test(React.createElement(StringState), 'SPAN', ''); + assertConsoleErrorDev([ + 'StringState.state: must be set to an object or null\n' + + ' in StringState (at **)', + ]); + test(React.createElement(NumberState), 'SPAN', ''); + assertConsoleErrorDev([ + 'NumberState.state: must be set to an object or null\n' + + ' in NumberState (at **)', + ]); }); - it('should render with null in the initial state property', function() { + it('should render with null in the initial state property', function () { test(React.createElement(NullState), 'SPAN', ''); }); - it('setState through an event handler', function() { + it('setState through an event handler', function () { test( React.createElement(BoundEventHandler, {initialValue: 'foo'}), 'DIV', - 'foo' + 'foo', ); ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); - it('should not implicitly bind event handlers', function() { + it('should not implicitly bind event handlers', function () { test( React.createElement(UnboundEventHandler, {initialValue: 'foo'}), 'DIV', - 'foo' + 'foo', ); expect(attachedListener).toThrow(); }); - it('renders using forceUpdate even when there is no state', function() { + it('renders using forceUpdate even when there is no state', function () { test( React.createElement(ForceUpdateWithNoState, {initialValue: 'foo'}), 'DIV', - 'foo' + 'foo', ); ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); - it('will call all the normal life cycle methods', function() { + it('will call all the normal life cycle methods', function () { lifeCycles = []; test(React.createElement(NormalLifeCycles, {value: 'foo'}), 'SPAN', 'foo'); expect(lifeCycles).toEqual(['will-mount', 'did-mount']); @@ -604,22 +621,29 @@ describe('ReactTypeScriptClass', function() { it( 'warns when classic properties are defined on the instance, ' + 'but does not invoke them.', - function() { + function () { getInitialStateWasCalled = false; getDefaultPropsWasCalled = false; - expect(() => - test(React.createElement(ClassicProperties), 'SPAN', 'foo') - ).toErrorDev([ - 'getInitialState was defined on ClassicProperties, ' + - 'a plain JavaScript class.', - 'getDefaultProps was defined on ClassicProperties, ' + - 'a plain JavaScript class.', - 'contextTypes was defined as an instance property on ClassicProperties.', - 'contextType was defined as an instance property on ClassicProperties.', + test(React.createElement(ClassicProperties), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'getInitialState was defined on ClassicProperties, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?\n' + + ' in ClassicProperties (at **)', + 'getDefaultProps was defined on ClassicProperties, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Use a static property to define defaultProps instead.\n' + + ' in ClassicProperties (at **)', + 'contextType was defined as an instance property on ClassicProperties. ' + + 'Use a static property to define contextType instead.\n' + + ' in ClassicProperties (at **)', + 'contextTypes was defined as an instance property on ClassicProperties. ' + + 'Use a static property to define contextTypes instead.\n' + + ' in ClassicProperties (at **)', ]); expect(getInitialStateWasCalled).toBe(false); expect(getDefaultPropsWasCalled).toBe(false); - } + }, ); } @@ -638,63 +662,73 @@ describe('ReactTypeScriptClass', function() { } test(React.createElement(Example), 'SPAN', 'foo'); - } + }, ); - it('should warn when misspelling shouldComponentUpdate', function() { - expect(() => - test(React.createElement(MisspelledComponent1), 'SPAN', 'foo') - ).toErrorDev( - '' + - 'MisspelledComponent1 has a method called componentShouldUpdate(). Did ' + + it('should warn when misspelling shouldComponentUpdate', function () { + test(React.createElement(MisspelledComponent1), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'MisspelledComponent1 has a method called componentShouldUpdate(). Did ' + 'you mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.' - ); + 'because the function is expected to return a value.\n' + + ' in MisspelledComponent1 (at **)', + ]); }); - it('should warn when misspelling componentWillReceiveProps', function() { - expect(() => - test(React.createElement(MisspelledComponent2), 'SPAN', 'foo') - ).toErrorDev( - '' + - 'MisspelledComponent2 has a method called componentWillRecieveProps(). ' + - 'Did you mean componentWillReceiveProps()?' - ); + it('should warn when misspelling componentWillReceiveProps', function () { + test(React.createElement(MisspelledComponent2), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'MisspelledComponent2 has a method called componentWillRecieveProps(). ' + + 'Did you mean componentWillReceiveProps()?\n' + + ' in MisspelledComponent2 (at **)', + ]); }); - it('should warn when misspelling UNSAFE_componentWillReceiveProps', function() { - expect(() => - test(React.createElement(MisspelledComponent3), 'SPAN', 'foo') - ).toErrorDev( - '' + - 'MisspelledComponent3 has a method called UNSAFE_componentWillRecieveProps(). ' + - 'Did you mean UNSAFE_componentWillReceiveProps()?' - ); + it('should warn when misspelling UNSAFE_componentWillReceiveProps', function () { + test(React.createElement(MisspelledComponent3), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'MisspelledComponent3 has a method called UNSAFE_componentWillRecieveProps(). ' + + 'Did you mean UNSAFE_componentWillReceiveProps()?\n' + + ' in MisspelledComponent3 (at **)', + ]); }); - it('should throw AND warn when trying to access classic APIs', function() { + it('should throw AND warn when trying to access classic APIs', function () { const ref = React.createRef(); test(React.createElement(Inner, {name: 'foo', ref: ref}), 'DIV', 'foo'); - expect(() => - expect(() => ref.current.replaceState({})).toThrow() - ).toWarnDev( - 'replaceState(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} + expect(() => ref.current.replaceState({})).toThrow(); + assertConsoleWarnDev( + [ + 'replaceState(...) is deprecated in plain JavaScript React classes. ' + + 'Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).', + ], + {withoutStack: true}, ); - expect(() => - expect(() => ref.current.isMounted()).toThrow() - ).toWarnDev( - 'isMounted(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} + expect(() => ref.current.isMounted()).toThrow(); + assertConsoleWarnDev( + [ + 'isMounted(...) is deprecated in plain JavaScript React classes. ' + + 'Instead, make sure to clean up subscriptions and pending requests in ' + + 'componentWillUnmount to prevent memory leaks.', + ], + {withoutStack: true}, ); }); if (!ReactFeatureFlags.disableLegacyContext) { it('supports this.context passed via getChildContext', () => { - expect(() => test(React.createElement(ProvideContext), 'DIV', 'bar-through-context')).toErrorDev([ - 'ProvideContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ReadContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', -] ); + test(React.createElement(ProvideContext), 'DIV', 'bar-through-context'); + assertConsoleErrorDev([ + 'ProvideContext uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ProvideContext (at **)', + 'ReadContext uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (ReactFeatureFlags.enableOwnerStacks + ? ' in ProvideContext.Object..ProvideContext (at **)' + : ' in ReadContext (at **)\n') + + ' in ProvideContext (at **)', + ]); }); } }); diff --git a/packages/react/src/__tests__/createReactClassIntegration-test.js b/packages/react/src/__tests__/createReactClassIntegration-test.js index 4aabc11b52..6bd3f06921 100644 --- a/packages/react/src/__tests__/createReactClassIntegration-test.js +++ b/packages/react/src/__tests__/createReactClassIntegration-test.js @@ -11,6 +11,7 @@ let act; let assertConsoleErrorDev; +let assertConsoleWarnDev; let PropTypes; let React; @@ -20,7 +21,11 @@ let createReactClass; describe('create-react-class-integration', () => { beforeEach(() => { jest.resetModules(); - ({act, assertConsoleErrorDev} = require('internal-test-utils')); + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); PropTypes = require('prop-types'); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -53,124 +58,130 @@ describe('create-react-class-integration', () => { }); it('should warn on invalid prop types', () => { - expect(() => - createReactClass({ - displayName: 'Component', - propTypes: { - prop: null, - }, - render: function () { - return {this.props.prop}; - }, - }), - ).toErrorDev( - 'Component: prop type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.', + createReactClass({ + displayName: 'Component', + propTypes: { + prop: null, + }, + render: function () { + return {this.props.prop}; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: Component: prop type `prop` is invalid; ' + + 'it must be a function, usually from React.PropTypes.', + ], {withoutStack: true}, ); }); it('should warn on invalid context types', () => { - expect(() => - createReactClass({ - displayName: 'Component', - contextTypes: { - prop: null, - }, - render: function () { - return {this.props.prop}; - }, - }), - ).toErrorDev( - 'Component: context type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.', + createReactClass({ + displayName: 'Component', + contextTypes: { + prop: null, + }, + render: function () { + return {this.props.prop}; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: Component: context type `prop` is invalid; ' + + 'it must be a function, usually from React.PropTypes.', + ], {withoutStack: true}, ); }); it('should throw on invalid child context types', () => { - expect(() => - createReactClass({ - displayName: 'Component', - childContextTypes: { - prop: null, - }, - render: function () { - return {this.props.prop}; - }, - }), - ).toErrorDev( - 'Component: child context type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.', + createReactClass({ + displayName: 'Component', + childContextTypes: { + prop: null, + }, + render: function () { + return {this.props.prop}; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: Component: child context type `prop` is invalid; it must be a function, usually from React.PropTypes.', + ], {withoutStack: true}, ); }); it('should warn when misspelling shouldComponentUpdate', () => { - expect(() => - createReactClass({ - componentShouldUpdate: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'A component has a method called componentShouldUpdate(). Did you ' + - 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.', + createReactClass({ + componentShouldUpdate: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: A component has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', + ], {withoutStack: true}, ); - expect(() => - createReactClass({ - displayName: 'NamedComponent', - componentShouldUpdate: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'NamedComponent has a method called componentShouldUpdate(). Did you ' + - 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.', + createReactClass({ + displayName: 'NamedComponent', + componentShouldUpdate: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: NamedComponent has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', + ], {withoutStack: true}, ); }); it('should warn when misspelling componentWillReceiveProps', () => { - expect(() => - createReactClass({ - componentWillRecieveProps: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'A component has a method called componentWillRecieveProps(). Did you ' + - 'mean componentWillReceiveProps()?', + createReactClass({ + componentWillRecieveProps: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: A component has a method called componentWillRecieveProps(). Did you ' + + 'mean componentWillReceiveProps()?', + ], {withoutStack: true}, ); }); it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => { - expect(() => - createReactClass({ - UNSAFE_componentWillRecieveProps: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'A component has a method called UNSAFE_componentWillRecieveProps(). ' + - 'Did you mean UNSAFE_componentWillReceiveProps()?', + createReactClass({ + UNSAFE_componentWillRecieveProps: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: A component has a method called UNSAFE_componentWillRecieveProps(). ' + + 'Did you mean UNSAFE_componentWillReceiveProps()?', + ], {withoutStack: true}, ); }); @@ -201,23 +212,22 @@ describe('create-react-class-integration', () => { // TODO: Consider actually moving these to statics or drop this unit test. // eslint-disable-next-line jest/no-disabled-tests it.skip('should warn when using deprecated non-static spec keys', () => { - expect(() => - createReactClass({ - mixins: [{}], - propTypes: { - foo: PropTypes.string, - }, - contextTypes: { - foo: PropTypes.string, - }, - childContextTypes: { - foo: PropTypes.string, - }, - render: function () { - return
; - }, - }), - ).toErrorDev([ + createReactClass({ + mixins: [{}], + propTypes: { + foo: PropTypes.string, + }, + contextTypes: { + foo: PropTypes.string, + }, + childContextTypes: { + foo: PropTypes.string, + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev([ '`mixins` is now a static property and should ' + 'be defined inside "statics".', '`propTypes` is now a static property and should ' + @@ -399,9 +409,12 @@ describe('create-react-class-integration', () => { }, }); - expect(() => expect(() => Component()).toThrow()).toErrorDev( - 'Something is calling a React component directly. Use a ' + - 'factory or JSX instead. See: https://fb.me/react-legacyfactory', + expect(() => Component()).toThrow(); + assertConsoleErrorDev( + [ + 'Warning: Something is calling a React component directly. Use a ' + + 'factory or JSX instead. See: https://fb.me/react-legacyfactory', + ], {withoutStack: true}, ); }); @@ -504,15 +517,15 @@ describe('create-react-class-integration', () => { return
; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getDerivedStateFromError is not static', async () => { @@ -525,15 +538,15 @@ describe('create-react-class-integration', () => { return
; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromError() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getSnapshotBeforeUpdate is static', async () => { @@ -548,15 +561,15 @@ describe('create-react-class-integration', () => { return
; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.', - ); + 'and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)', + ]); }); it('should warn if state is not properly initialized before getDerivedStateFromProps', async () => { @@ -571,17 +584,17 @@ describe('create-react-class-integration', () => { return null; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ '`Component` uses `getDerivedStateFromProps` but its initial state is ' + 'null. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `Component`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.', - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Component (at **)', + ]); }); it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', async () => { @@ -609,30 +622,52 @@ describe('create-react-class-integration', () => { }); Component.displayName = 'Component'; - await expect(async () => { - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' componentWillReceiveProps\n' + - ' componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in Component (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', ], {withoutStack: true}, ); - const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(); }); @@ -659,26 +694,49 @@ describe('create-react-class-integration', () => { }); Component.displayName = 'Component'; - await expect(async () => { - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' componentWillReceiveProps\n' + - ' componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in Component (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', ], {withoutStack: true}, ); @@ -721,15 +779,38 @@ describe('create-react-class-integration', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toWarnDev( + await act(() => { + root.render(); + }); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', ], {withoutStack: true}, ); @@ -803,14 +884,16 @@ describe('create-react-class-integration', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'MyComponent: isMounted is deprecated. Instead, make sure to ' + - 'clean up subscriptions and pending requests in componentWillUnmount ' + - 'to prevent memory leaks.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + [ + 'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' + + 'clean up subscriptions and pending requests in componentWillUnmount ' + + 'to prevent memory leaks.\n' + + ' in MyComponent (at **)', + ], // This now has a component stack even though it's part of a third-party library. ); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 3046f8f4cb..0720ab2a88 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -78,7 +78,8 @@ export const enableLegacyFBSupport = false; // ----------------------------------------------------------------------------- // Yield to the browser event loop and not just the scheduler event loop before passive effects. -export const enableYieldingBeforePassive = __EXPERIMENTAL__; +// Fix gated tests that fail with this flag enabled before turning it back on. +export const enableYieldingBeforePassive = false; export const enableLegacyCache = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 8d38112b16..81060cfafb 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -68,7 +68,7 @@ export const enableFabricCompleteRootInCommitPhase = false; export const enableSiblingPrerendering = true; export const enableUseResourceEffectHook = true; export const enableHydrationLaneScheduling = true; -export const enableYieldingBeforePassive = true; +export const enableYieldingBeforePassive = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 28a303a034..e0e9906d52 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -82,7 +82,7 @@ export const enableUseResourceEffectHook = false; export const enableHydrationLaneScheduling = true; -export const enableYieldingBeforePassive = true; +export const enableYieldingBeforePassive = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/scripts/jest/spec-equivalence-reporter/setupTests.js b/scripts/jest/spec-equivalence-reporter/setupTests.js index 487e0d3003..c3eaf5690a 100644 --- a/scripts/jest/spec-equivalence-reporter/setupTests.js +++ b/scripts/jest/spec-equivalence-reporter/setupTests.js @@ -7,6 +7,11 @@ 'use strict'; +const { + patchConsoleMethods, + resetAllUnexpectedConsoleCalls, + flushAllUnexpectedConsoleCalls, +} = require('internal-test-utils/consoleMock'); const spyOn = jest.spyOn; // Spying on console methods in production builds can mask errors. @@ -36,6 +41,11 @@ global.spyOnProd = function (...args) { } }; +// Patch the console to assert that all console error/warn/log calls assert. +patchConsoleMethods({includeLog: !!process.env.CI}); +beforeEach(resetAllUnexpectedConsoleCalls); +afterEach(flushAllUnexpectedConsoleCalls); + expect.extend({ ...require('../matchers/reactTestMatchers'), ...require('../matchers/toThrow'),