From 9f540fcc51eae6fb6eab8d4ccba00cb0477a6b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 19 Dec 2024 12:54:59 -0500 Subject: [PATCH 01/19] [Flight] Support streaming of decodeReply in Edge environments (#31852) We support streaming `multipart/form-data` in Node.js using Busboy since that's kind of the idiomatic ecosystem way for handling these stream there. There's not really anything idiomatic like that for Edge that's universal yet. This adds a version that's basically just `AsyncIterable.from(formData)`. It could also be a `ReadableStream` of those entries since those are also `AsyncIterable`. I imagine that in the future we might add one from a binary `ReadableStream` that does the parsing built-in. --- .../npm/server.edge.js | 1 + .../react-server-dom-parcel/server.edge.js | 1 + .../src/server/ReactFlightDOMServerEdge.js | 49 ++++++++++++++++++ .../server/react-flight-dom-server.edge.js | 1 + .../npm/server.edge.js | 1 + .../react-server-dom-turbopack/server.edge.js | 1 + .../src/server/ReactFlightDOMServerEdge.js | 51 +++++++++++++++++++ .../server/react-flight-dom-server.edge.js | 1 + .../npm/server.edge.js | 1 + .../react-server-dom-webpack/server.edge.js | 1 + .../__tests__/ReactFlightDOMReplyEdge-test.js | 36 +++++++++++++ .../src/server/ReactFlightDOMServerEdge.js | 51 +++++++++++++++++++ .../server/react-flight-dom-server.edge.js | 1 + 13 files changed, 196 insertions(+) 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__/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, From c70ab3f4b051348e3dd91144d9c7299a2e2311a5 Mon Sep 17 00:00:00 2001 From: lauren Date: Thu, 19 Dec 2024 13:03:11 -0500 Subject: [PATCH 02/19] [ci] getWorkflowRun should not throw early if workflow hasn't completed (#31861) We already have handling and retry logic for in-flight workflows in `downloadArtifactsFromGitHub`, so there's no need to exit early if we find a workflow for a given commit but it hasn't finished yet. --- .../release/shared-commands/download-build-artifacts.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scripts/release/shared-commands/download-build-artifacts.js b/scripts/release/shared-commands/download-build-artifacts.js index 85cbb8b9fa..cf73016880 100644 --- a/scripts/release/shared-commands/download-build-artifacts.js +++ b/scripts/release/shared-commands/download-build-artifacts.js @@ -43,12 +43,7 @@ async function getWorkflowRun(commit) { ); const json = JSON.parse(res.stdout); - const workflowRun = json.workflow_runs.find( - run => - run.head_sha === commit && - run.status === 'completed' && - run.conclusion === 'success' - ); + const workflowRun = json.workflow_runs.find(run => run.head_sha === commit); if (workflowRun == null || workflowRun.id == null) { console.log( From 36d15d58628baf5e15624a52febae873a7a56345 Mon Sep 17 00:00:00 2001 From: Ricky Date: Thu, 19 Dec 2024 13:05:23 -0500 Subject: [PATCH 03/19] [assert helpers] ReactChildren-test (#31844) Based off https://github.com/facebook/react/pull/31843 Commit to review: https://github.com/facebook/react/pull/31844/commits/2c653b81a73e155f1548c0362e5334629a45351e Moar tests --- .../react/src/__tests__/ReactChildren-test.js | 206 +++++++++++------- 1 file changed, 122 insertions(+), 84 deletions(-) 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 **)', - ); + ]); }); }); }); From 518d06d26a97df6d4f5b04e529e5018ad35ea936 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 19 Dec 2024 20:43:01 +0100 Subject: [PATCH 04/19] Turn off `enableYieldingBeforePassive` (#31857) --- .../ReactSuspenseyCommitPhase-test.js | 42 +++++++++++++++++++ packages/shared/ReactFeatureFlags.js | 3 +- 2 files changed, 44 insertions(+), 1 deletion(-) 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/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__; From de82912e620518d501680bbd93fbb5cc8d134223 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Fri, 20 Dec 2024 09:48:50 -0500 Subject: [PATCH 05/19] Turn off enableYieldingBeforePassive in internal test renderers (#31863) https://github.com/facebook/react/pull/31785 turned on `enableYieldingBeforePassive` for the internal test renderer builds. We have some failing tests on the RN side blocking the sync so lets turn these off for now. --- .../shared/forks/ReactFeatureFlags.test-renderer.native-fb.js | 2 +- packages/shared/forks/ReactFeatureFlags.test-renderer.www.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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); From 6a3d6a4382cdafc1260483a6fc5f76593fc038e4 Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:56:48 -0800 Subject: [PATCH 06/19] [compiler] Allow type cast expressions with refs (#31871) We report a false positive for the combination of a ref-accessing function placed inside an array which is they type-cast. Here we teach ref validation about type casts. I also tried other variants like `return ref as const` but those already worked. Closes #31864 --- .../Validation/ValidateNoRefAccesInRender.ts | 8 +++ .../allow-ref-type-cast-in-render.expect.md | 60 +++++++++++++++++++ .../compiler/allow-ref-type-cast-in-render.js | 17 ++++++ 3 files changed, 85 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js 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: [{}], +}; From 26297f5383f7e7150d9aa2cf12e8326c96991cab Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 20 Dec 2024 12:41:13 -0500 Subject: [PATCH 07/19] [assert helpers] not dom or reconciler (#31862) converts everything left outside react-dom and react-reconciler --- .../__tests__/ReactCacheOld-test.internal.js | 31 +++++-- .../ReactHooksInspectionIntegration-test.js | 13 +-- .../__tests__/trustedTypes-test.internal.js | 14 +-- .../__tests__/ReactFabric-test.internal.js | 86 ++++++++++--------- .../ReactNativeEvents-test.internal.js | 50 ++++++----- .../ReactNativeMount-test.internal.js | 33 +++---- .../__tests__/ReactFlightDOMBrowser-test.js | 52 +++++++---- .../src/__tests__/ReactFlightDOMEdge-test.js | 21 +++-- .../src/__tests__/ReactFlightDOMForm-test.js | 13 ++- .../src/__tests__/ReactTestRenderer-test.js | 23 +++-- 10 files changed, 193 insertions(+), 143 deletions(-) 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-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-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-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 From 99471c02dd6631df1892bf76d932afd22fffa5e3 Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 20 Dec 2024 12:41:30 -0500 Subject: [PATCH 08/19] [assert helpers] ReactFlight (#31860) --- .../src/__tests__/ReactFlight-test.js | 640 +++++++++++++----- 1 file changed, 474 insertions(+), 166 deletions(-) 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'); From 03297e048d08de2f7c4c0d2950e2cb1c13875f66 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 20 Dec 2024 15:09:09 -0500 Subject: [PATCH 09/19] [compiler] transform fire calls (#31796) This is the diff with the meaningful changes. The approach is: 1. Collect fire callees and remove fire() calls, create a new binding for the useFire result 2. Update LoadLocals for captured callees to point to the useFire result 3. Update function context to reference useFire results 4. Insert useFire calls after getting to the component scope This approach aims to minimize the amount of new bindings we introduce for the function expressions to minimize bookkeeping for dependency arrays. We keep all of the LoadLocals leading up to function calls as they are and insert new instructions to load the originally captured function, call useFire, and store the result in a new promoted temporary. The lvalues that referenced the original callee are changed to point to the new useFire result. This is the minimal diff to implement the expected behavior (up to importing the useFire call, next diff) and further stacked diffs implement error handling. The rules for fire are: 1. If you use fire for a callee in the effect once you must use it for every time you call it in that effect 2. You can only use fire in a useEffect lambda/functions defined inside the useEffect lambda There is still more work to do here, like updating the effect dependency array and handling object methods -- --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31796). * #31811 * #31798 * #31797 * __->__ #31796 --- .../src/Entrypoint/Pipeline.ts | 6 + .../src/Transform/TransformFire.ts | 613 ++++++++++++++++++ .../src/Transform/index.ts | 7 + .../compiler/transform-fire/basic.expect.md | 52 ++ .../fixtures/compiler/transform-fire/basic.js | 13 + .../transform-fire/deep-scope.expect.md | 73 +++ .../compiler/transform-fire/deep-scope.js | 22 + ...r.invalid-conditional-use-effect.expect.md | 37 ++ .../error.invalid-conditional-use-effect.js | 16 + .../error.invalid-multiple-args.expect.md | 34 + .../error.invalid-multiple-args.js | 13 + .../error.invalid-nested-use-effect.expect.md | 40 ++ .../error.invalid-nested-use-effect.js | 19 + .../error.invalid-not-call.expect.md | 34 + .../transform-fire/error.invalid-not-call.js | 13 + .../error.invalid-spread.expect.md | 34 + .../transform-fire/error.invalid-spread.js | 13 + .../error.todo-method.expect.md | 34 + .../transform-fire/error.todo-method.js | 13 + .../transform-fire/multiple-scope.expect.md | 65 ++ .../compiler/transform-fire/multiple-scope.js | 21 + .../transform-fire/repeated-calls.expect.md | 61 ++ .../compiler/transform-fire/repeated-calls.js | 14 + .../shared-hook-calls.expect.md | 80 +++ .../transform-fire/shared-hook-calls.js | 18 + .../use-effect-no-args-no-op.expect.md | 30 + .../use-effect-no-args-no-op.js | 8 + 27 files changed, 1383 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.js 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/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts new file mode 100644 index 0000000000..3fbd141212 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -0,0 +1,613 @@ +/** + * 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} from '..'; +import { + 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'; + +/* + * TODO(jmbrown): + * In this stack: + * - Insert useFire import + * - Assert no lingering fire calls + * - Ensure a fired function is not called regularly elsewhere in the same effect + * + * Future: + * - rewrite dep arrays + * - 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); + 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); + } + } + } + } 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); + + 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); + } + } + block.instructions = rewriteInstructions(rewriteInstrs, block.instructions); + block.instructions = deleteInstructions(deleteInstrs, block.instructions); + + if (rewriteInstrs.size > 0 || deleteInstrs.size > 0) { + hasRewrite = 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; +} + +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; + } +>; + +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; + } + + 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): Place { + const fireFunctionBinding = getOrInsertWith( + this.#fireCalleesToFireFunctions, + callee.identifier.id, + () => createTemporaryPlace(this.#env, GeneratedSource), + ); + + this.#capturedCalleeIdentifierIds.set(callee.identifier.id, { + fireFunctionBinding, + capturedCalleeIdentifier: callee.identifier, + }); + + 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); + } + + throwIfErrorsFound(): void { + if (this.#errors.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/__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..f3b67da3ec --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md @@ -0,0 +1,52 @@ + +## 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 { 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..ee9bf268d0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md @@ -0,0 +1,73 @@ + +## 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 { 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-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-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/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..08c45ea279 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md @@ -0,0 +1,65 @@ + +## 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 { 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..693a8d380a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md @@ -0,0 +1,61 @@ + +## 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 { 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/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..959338b5d8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md @@ -0,0 +1,80 @@ + +## 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 { 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; +} From ab27231dc51aa2535df37555797e630d31047fa4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 20 Dec 2024 15:25:30 -0500 Subject: [PATCH 10/19] [compiler] add fire imports (#31797) Summary: Adds import {useFire} from 'react' when fire syntax is used. This is experimentation and may not become a stable feature in the compiler. -- --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31797). * #31811 * #31798 * __->__ #31797 --- .../babel-plugin-react-compiler/src/Entrypoint/Program.ts | 5 +++++ .../babel-plugin-react-compiler/src/HIR/Environment.ts | 2 ++ .../src/ReactiveScopes/CodegenReactiveFunction.ts | 6 ++++++ .../src/Transform/TransformFire.ts | 2 +- .../fixtures/compiler/transform-fire/basic.expect.md | 1 + .../fixtures/compiler/transform-fire/deep-scope.expect.md | 1 + .../compiler/transform-fire/multiple-scope.expect.md | 1 + .../compiler/transform-fire/repeated-calls.expect.md | 1 + .../compiler/transform-fire/shared-hook-calls.expect.md | 1 + 9 files changed, 19 insertions(+), 1 deletion(-) 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..ca10e9c0d4 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,11 @@ 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: 'react', 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/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 index 3fbd141212..5c7f906be8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -32,7 +32,6 @@ import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape'; /* * TODO(jmbrown): * In this stack: - * - Insert useFire import * - Assert no lingering fire calls * - Ensure a fired function is not called regularly elsewhere in the same effect * @@ -226,6 +225,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { if (rewriteInstrs.size > 0 || deleteInstrs.size > 0) { hasRewrite = true; + fn.env.hasFireRewrite = true; } } 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 index f3b67da3ec..a5bf42de7b 100644 --- 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 @@ -21,6 +21,7 @@ function Component(props) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; 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 index ee9bf268d0..585a820b13 100644 --- 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 @@ -30,6 +30,7 @@ function Component(props) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; 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 index 08c45ea279..0d53104135 100644 --- 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 @@ -29,6 +29,7 @@ function Component(props) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; 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 index 693a8d380a..3eb9f0e71c 100644 --- 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 @@ -22,6 +22,7 @@ function Component(props) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; 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 index 959338b5d8..4362a3a846 100644 --- 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 @@ -26,6 +26,7 @@ function Component({bar, baz}) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; From 45a720f7c7ff98e22fb299b50fef90fe319081a7 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 20 Dec 2024 16:55:01 -0500 Subject: [PATCH 11/19] [compile] Error on fire outside of effects and ensure correct compilation, correct import (#31798) Traverse the compiled functions to ensure there are no lingering fires and that all fire calls are inside an effect lambda. Also corrects the import to import from the compiler runtime instead -- --- .../src/Entrypoint/Program.ts | 5 +- .../src/HIR/PrintHIR.ts | 8 ++ .../src/Transform/TransformFire.ts | 104 ++++++++++++++++-- .../compiler/transform-fire/basic.expect.md | 2 +- .../transform-fire/deep-scope.expect.md | 2 +- ...ror.invalid-mix-fire-and-no-fire.expect.md | 39 +++++++ .../error.invalid-mix-fire-and-no-fire.js | 18 +++ .../error.invalid-outside-effect.expect.md | 38 +++++++ .../error.invalid-outside-effect.js | 15 +++ .../transform-fire/multiple-scope.expect.md | 2 +- .../transform-fire/repeated-calls.expect.md | 2 +- .../shared-hook-calls.expect.md | 2 +- 12 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.js 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 ca10e9c0d4..bb0d662c4f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -567,7 +567,10 @@ export function compileProgram( const hasFireRewrite = compiledFns.some(c => c.compiledFn.hasFireRewrite); if (environment.enableFire && hasFireRewrite) { - externalFunctions.push({source: 'react', importSpecifierName: 'useFire'}); + externalFunctions.push({ + source: getReactCompilerRuntimeModule(pass.opts), + importSpecifierName: 'useFire', + }); } } catch (err) { handleError(err, pass, null); 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/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts index 5c7f906be8..a0256fd80c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, CompilerErrorDetailOptions, ErrorSeverity} from '..'; +import { + CompilerError, + CompilerErrorDetailOptions, + ErrorSeverity, + SourceLocation, +} from '..'; import { CallExpression, Effect, @@ -28,14 +33,11 @@ import { 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): - * In this stack: - * - Assert no lingering fire calls - * - Ensure a fired function is not called regularly elsewhere in the same effect - * - * Future: * - rewrite dep arrays * - traverse object methods * - method calls @@ -47,6 +49,9 @@ 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(); } @@ -120,6 +125,11 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { } rewriteInstrs.set(loadUseEffectInstrId, newInstrs); } + ensureNoRemainingCalleeCaptures( + lambda.loweredFunc.func, + context, + capturedCallees, + ); } } } else if ( @@ -159,7 +169,10 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { } const fireFunctionBinding = - context.getOrGenerateFireFunctionBinding(loadLocal.place); + context.getOrGenerateFireFunctionBinding( + loadLocal.place, + value.loc, + ); loadLocal.place = {...fireFunctionBinding}; @@ -320,6 +333,69 @@ function visitFunctionExpressionAndPropagateFireDependencies( 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; @@ -422,6 +498,7 @@ type FireCalleesToFireFunctionBinding = Map< { fireFunctionBinding: Place; capturedCalleeIdentifier: Identifier; + fireLoc: SourceLocation; } >; @@ -523,8 +600,10 @@ class Context { getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined { return this.#loadLocals.get(id); } - - getOrGenerateFireFunctionBinding(callee: Place): Place { + getOrGenerateFireFunctionBinding( + callee: Place, + fireLoc: SourceLocation, + ): Place { const fireFunctionBinding = getOrInsertWith( this.#fireCalleesToFireFunctions, callee.identifier.id, @@ -534,6 +613,7 @@ class Context { this.#capturedCalleeIdentifierIds.set(callee.identifier.id, { fireFunctionBinding, capturedCalleeIdentifier: callee.identifier, + fireLoc, }); return fireFunctionBinding; @@ -575,8 +655,12 @@ class Context { return this.#loadGlobalInstructionIds.get(id); } + hasErrors(): boolean { + return this.#errors.hasErrors(); + } + throwIfErrorsFound(): void { - if (this.#errors.hasErrors()) throw this.#errors; + if (this.hasErrors()) throw this.#errors; } } 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 index a5bf42de7b..8d8bc179a2 100644 --- 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 @@ -21,7 +21,7 @@ function Component(props) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; 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 index 585a820b13..a335fea886 100644 --- 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 @@ -30,7 +30,7 @@ function Component(props) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; 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-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/multiple-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md index 0d53104135..02f3935171 100644 --- 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 @@ -29,7 +29,7 @@ function Component(props) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; 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 index 3eb9f0e71c..1734ca3ab4 100644 --- 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 @@ -22,7 +22,7 @@ function Component(props) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; 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 index 4362a3a846..9b689b31c7 100644 --- 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 @@ -26,7 +26,7 @@ function Component({bar, baz}) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; From 6907aa2a309bdc47dc3504683159cb50b590eed8 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 20 Dec 2024 17:16:59 -0500 Subject: [PATCH 12/19] [compiler] Rewrite effect dep arrays that use fire (#31811) If an effect uses a dep array, also rewrite the dep array to use the fire binding -- --- .../src/Transform/TransformFire.ts | 73 +++++++++++++++++-- ...id-rewrite-deps-no-array-literal.expect.md | 37 ++++++++++ ...r.invalid-rewrite-deps-no-array-literal.js | 16 ++++ ...rror.invalid-rewrite-deps-spread.expect.md | 37 ++++++++++ .../error.invalid-rewrite-deps-spread.js | 19 +++++ .../fire-and-autodeps.expect.md | 60 +++++++++++++++ .../transform-fire/fire-and-autodeps.js | 13 ++++ .../transform-fire/rewrite-deps.expect.md | 57 +++++++++++++++ .../compiler/transform-fire/rewrite-deps.js | 13 ++++ 9 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts index a0256fd80c..a35c4ddb01 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -12,6 +12,7 @@ import { SourceLocation, } from '..'; import { + ArrayExpression, CallExpression, Effect, Environment, @@ -38,7 +39,6 @@ import {printSourceLocationLine} from '../HIR/PrintHIR'; /* * TODO(jmbrown): - * - rewrite dep arrays * - traverse object methods * - method calls * - React.useEffect calls @@ -125,11 +125,58 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { } rewriteInstrs.set(loadUseEffectInstrId, newInstrs); } - ensureNoRemainingCalleeCaptures( - lambda.loweredFunc.func, - context, - capturedCallees, + } + 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 ( @@ -231,6 +278,8 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { 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); @@ -561,6 +610,12 @@ class Context { 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); } @@ -655,6 +710,14 @@ class Context { 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(); } 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/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/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; +} From 94867f33be327a52bfffda89a14c85897180e43e Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 23 Dec 2024 14:58:20 -0500 Subject: [PATCH 13/19] [asserts helpers] react package (#31853) Based off https://github.com/facebook/react/pull/31844 Commit to review: https://github.com/facebook/react/commit/11aa104e3e70c0accc21f785060b812beb145089 Converts the rest of the `react` package. --- .../src/__tests__/ReactLazy-test.internal.js | 52 +- .../ReactCoffeeScriptClass-test.coffee | 217 ++++---- .../__tests__/ReactContextValidator-test.js | 323 ++++++------ .../src/__tests__/ReactCreateElement-test.js | 66 ++- .../src/__tests__/ReactCreateRef-test.js | 40 +- .../react/src/__tests__/ReactES6Class-test.js | 159 ++++-- .../src/__tests__/ReactElementClone-test.js | 29 +- .../ReactElementValidator-test.internal.js | 321 +++++++----- .../ReactJSXElementValidator-test.js | 163 +++--- .../src/__tests__/ReactJSXRuntime-test.js | 103 ++-- .../ReactJSXTransformIntegration-test.js | 12 +- .../ReactProfilerComponent-test.internal.js | 15 +- .../src/__tests__/ReactPureComponent-test.js | 38 +- .../__tests__/ReactStartTransition-test.js | 29 +- .../src/__tests__/ReactStrictMode-test.js | 112 ++-- .../__tests__/ReactTypeScriptClass-test.ts | 282 ++++++----- .../createReactClassIntegration-test.js | 479 ++++++++++-------- .../spec-equivalence-reporter/setupTests.js | 10 + 18 files changed, 1434 insertions(+), 1016 deletions(-) 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/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/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'), From 97d794958f5b19b66a980f737facd890463f0cb8 Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 23 Dec 2024 18:11:04 -0500 Subject: [PATCH 14/19] [assert helpers] Remove toWarnDev from fixtures/dom (#31894) This is unused and never was: https://github.com/facebook/react/commit/e6a0473c3c6f501dbe291f60b9ee35760ab99eed --- fixtures/dom/src/__tests__/nested-act-test.js | 2 - fixtures/dom/src/toWarnDev.js | 284 ------------------ 2 files changed, 286 deletions(-) delete mode 100644 fixtures/dom/src/toWarnDev.js 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'), -}; From 1ebc9890a47e3801262064f541c6f4379fc499b5 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Mon, 23 Dec 2024 18:23:20 -0500 Subject: [PATCH 15/19] [rcr] Relax react peer dep requirement There's no real reason to restrict the React peer dep to non-experimental, so relax it. --- compiler/packages/react-compiler-runtime/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 3097993b00fb147f4dd3630de15c27ade293d210 Mon Sep 17 00:00:00 2001 From: lauren Date: Mon, 23 Dec 2024 18:23:20 -0500 Subject: [PATCH 16/19] [compiler] Add support for canonical reactrc configs This PR adds experimental support for a canoncial reactrc config file to be provided. This will be used later by other tooling such as a compiler upgrade script, IDE extension and so on, as the canonical configuration source for the compiler. --- .../babel-plugin-react-compiler/package.json | 1 + .../src/Babel/BabelPlugin.ts | 20 +++++++++++++++++-- .../src/Entrypoint/Options.ts | 9 +++++++++ compiler/yarn.lock | 17 +++++++++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) 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/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/yarn.lock b/compiler/yarn.lock index b4c72ff3c5..7f06d6c6eb 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -3841,6 +3841,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" @@ -4076,6 +4086,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" @@ -4758,7 +4773,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== From e21110e2b83210344f7223c223e23e56e52b0f75 Mon Sep 17 00:00:00 2001 From: lauren Date: Mon, 23 Dec 2024 18:23:20 -0500 Subject: [PATCH 17/19] [forgive] Scaffold workspaces Basic workspace setup for Forgive. --- .../react-forgive/client/package.json | 22 + .../packages/react-forgive/client/yarn.lock | 59 +++ compiler/packages/react-forgive/package.json | 59 +++ .../react-forgive/server/package.json | 20 + .../react-forgive/server/src/index.ts | 6 + .../react-forgive/server/tsconfig.json | 13 + .../packages/react-forgive/server/yarn.lock | 33 ++ compiler/yarn.lock | 460 +++++++++++++++++- 8 files changed, 671 insertions(+), 1 deletion(-) create mode 100644 compiler/packages/react-forgive/client/package.json create mode 100644 compiler/packages/react-forgive/client/yarn.lock create mode 100644 compiler/packages/react-forgive/package.json create mode 100644 compiler/packages/react-forgive/server/package.json create mode 100644 compiler/packages/react-forgive/server/src/index.ts create mode 100644 compiler/packages/react-forgive/server/tsconfig.json create mode 100644 compiler/packages/react-forgive/server/yarn.lock diff --git a/compiler/packages/react-forgive/client/package.json b/compiler/packages/react-forgive/client/package.json new file mode 100644 index 0000000000..c90cee4b42 --- /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-client" + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/vscode": "^1.95.0" + } +} 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..fc01a9325e --- /dev/null +++ b/compiler/packages/react-forgive/package.json @@ -0,0 +1,59 @@ +{ + "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": "yarn run esbuild-base -- --sourcemap", + "dev": "yarn run package && yarn run install-ext", + "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node", + "install-ext": "code --install-extension vscode-react-compiler-0.0.1.vsix", + "lint": "eslint src --ext ts", + "package": "vsce package", + "postinstall": "cd client && yarn install && cd ../server && yarn install && cd ..", + "pretest": "yarn run compile && yarn run lint", + "test": "vscode-test", + "test-compile": "tsc -p ./", + "vscode:prepublish": "yarn run esbuild-base -- --minify", + "watch": "yarn run esbuild-base -- --sourcemap --watch" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/node": "^20", + "esbuild": "^0.24.0", + "eslint": "^9.13.0", + "typescript": "^5.7.2", + "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..4e4d54debb --- /dev/null +++ b/compiler/packages/react-forgive/server/package.json @@ -0,0 +1,20 @@ +{ + "private": "true", + "name": "react-forgive-server", + "version": "0.0.0", + "description": "Experimental LSP server", + "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-server" + }, + "dependencies": { + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-textdocument": "^1.0.12" + } +} 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..a265a953ee --- /dev/null +++ b/compiler/packages/react-forgive/server/src/index.ts @@ -0,0 +1,6 @@ +/** + * 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. + */ diff --git a/compiler/packages/react-forgive/server/tsconfig.json b/compiler/packages/react-forgive/server/tsconfig.json new file mode 100644 index 0000000000..ccd17f5dff --- /dev/null +++ b/compiler/packages/react-forgive/server/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@tsconfig/strictest/tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node", + "outDir": "dist", + "jsx": "react-jsxdev", + "lib": ["ES2020"], + "target": "ES2020", + }, + "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..fd60ddd6ad --- /dev/null +++ b/compiler/packages/react-forgive/server/yarn.lock @@ -0,0 +1,33 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +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" diff --git a/compiler/yarn.lock b/compiler/yarn.lock index 7f06d6c6eb..4f6f6ac4b0 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" @@ -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" @@ -3865,6 +4159,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" @@ -4103,6 +4406,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" @@ -4148,6 +4481,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" @@ -4163,6 +4504,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" @@ -4251,6 +4597,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" @@ -4272,6 +4667,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" @@ -4430,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" @@ -4477,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" @@ -4630,6 +5052,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" @@ -6169,6 +6596,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" @@ -6210,6 +6642,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" @@ -7591,11 +8030,30 @@ 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== +typescript@^5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== + +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" From 36eefccef5142cbf3ab776f8287049ac611db2e7 Mon Sep 17 00:00:00 2001 From: lauren Date: Thu, 26 Dec 2024 13:40:31 -0500 Subject: [PATCH 18/19] [forgive] Init Init basic LSP. At the moment the extension doesn't do anything interesting, but it does compile successfully. --- .gitignore | 2 + .../rollup.config.js | 2 +- compiler/packages/react-forgive/.vscodeignore | 3 + compiler/packages/react-forgive/.yarnrc | 1 + compiler/packages/react-forgive/LICENSE | 21 + .../react-forgive/client/package.json | 2 +- .../react-forgive/client/src/extension.ts | 62 +++ compiler/packages/react-forgive/package.json | 15 +- .../react-forgive/server/package.json | 11 +- .../server/src/compiler/index.ts | 58 +++ .../server/src/compiler/options.ts | 25 ++ .../react-forgive/server/src/index.ts | 87 ++++ .../react-forgive/server/tsconfig.json | 16 +- .../packages/react-forgive/server/yarn.lock | 377 ++++++++++++++++++ compiler/yarn.lock | 5 - 15 files changed, 664 insertions(+), 23 deletions(-) create mode 100644 compiler/packages/react-forgive/.vscodeignore create mode 100644 compiler/packages/react-forgive/.yarnrc create mode 100644 compiler/packages/react-forgive/LICENSE create mode 100644 compiler/packages/react-forgive/client/src/extension.ts create mode 100644 compiler/packages/react-forgive/server/src/compiler/index.ts create mode 100644 compiler/packages/react-forgive/server/src/compiler/options.ts 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/packages/babel-plugin-react-compiler/rollup.config.js b/compiler/packages/babel-plugin-react-compiler/rollup.config.js index 77e785c464..58b2709d55 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', 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 index c90cee4b42..a975439726 100644 --- a/compiler/packages/react-forgive/client/package.json +++ b/compiler/packages/react-forgive/client/package.json @@ -11,7 +11,7 @@ "repository": { "type": "git", "url": "git+https://github.com/facebook/react.git", - "directory": "compiler/packages/react-forgive-client" + "directory": "compiler/packages/react-forgive" }, "dependencies": { "vscode-languageclient": "^9.0.1" 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/package.json b/compiler/packages/react-forgive/package.json index fc01a9325e..1c6e941b9d 100644 --- a/compiler/packages/react-forgive/package.json +++ b/compiler/packages/react-forgive/package.json @@ -35,25 +35,24 @@ ] }, "scripts": { - "compile": "yarn run esbuild-base -- --sourcemap", + "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-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node", - "install-ext": "code --install-extension vscode-react-compiler-0.0.1.vsix", + "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": "vsce package", + "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", - "test-compile": "tsc -p ./", - "vscode:prepublish": "yarn run esbuild-base -- --minify", - "watch": "yarn run esbuild-base -- --sourcemap --watch" + "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": "^5.7.2", "typescript-eslint": "^8.16.0" } } diff --git a/compiler/packages/react-forgive/server/package.json b/compiler/packages/react-forgive/server/package.json index 4e4d54debb..5e120f0271 100644 --- a/compiler/packages/react-forgive/server/package.json +++ b/compiler/packages/react-forgive/server/package.json @@ -4,16 +4,23 @@ "version": "0.0.0", "description": "Experimental LSP server", "license": "MIT", + "main": "dist/index.js", "scripts": { - "build": "echo 'no build'", + "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-server" + "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 index a265a953ee..b86a07292e 100644 --- a/compiler/packages/react-forgive/server/src/index.ts +++ b/compiler/packages/react-forgive/server/src/index.ts @@ -4,3 +4,90 @@ * 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 index ccd17f5dff..88422f5c91 100644 --- a/compiler/packages/react-forgive/server/tsconfig.json +++ b/compiler/packages/react-forgive/server/tsconfig.json @@ -1,13 +1,17 @@ { "extends": "@tsconfig/strictest/tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "node", - "outDir": "dist", + "module": "ES2015", + "moduleResolution": "Bundler", + "rootDir": "../../..", + "noEmit": true, "jsx": "react-jsxdev", - "lib": ["ES2020"], - "target": "ES2020", + "target": "ES2015", + "sourceMap": false, + "removeComments": true, + + "strictNullChecks": false }, "exclude": ["node_modules", ".vscode-test"], - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts"] } diff --git a/compiler/packages/react-forgive/server/yarn.lock b/compiler/packages/react-forgive/server/yarn.lock index fd60ddd6ad..b72063294f 100644 --- a/compiler/packages/react-forgive/server/yarn.lock +++ b/compiler/packages/react-forgive/server/yarn.lock @@ -2,6 +2,378 @@ # 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" @@ -31,3 +403,8 @@ vscode-languageserver@^9.0.1: 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 4f6f6ac4b0..4db4111027 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -8044,11 +8044,6 @@ typescript@^5.4.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== -typescript@^5.7.2: - version "5.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" - integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== - undici-types@~6.19.2: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" From c9cfbfda7e6efb4e9183bf554e7f4e01c05970a6 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Thu, 26 Dec 2024 13:41:30 -0500 Subject: [PATCH 19/19] [compiler] Update rollup plugins Update our various compiler rollup plugins. --- compiler/package.json | 6 +- .../rollup.config.js | 1 + .../rollup.config.js | 1 + .../rollup.config.js | 1 + .../react-compiler-runtime/rollup.config.js | 1 + compiler/yarn.lock | 75 ++++++++----------- 6 files changed, 38 insertions(+), 47 deletions(-) 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/rollup.config.js b/compiler/packages/babel-plugin-react-compiler/rollup.config.js index 58b2709d55..bccc69f423 100644 --- a/compiler/packages/babel-plugin-react-compiler/rollup.config.js +++ b/compiler/packages/babel-plugin-react-compiler/rollup.config.js @@ -24,6 +24,7 @@ const DEV_ROLLUP_CONFIG = { format: 'cjs', sourcemap: false, exports: 'named', + inlineDynamicImports: true, }, plugins: [ typescript({ 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/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/yarn.lock b/compiler/yarn.lock index 4db4111027..d31672dbfd 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -2708,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" @@ -2727,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" @@ -2748,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" @@ -3915,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" @@ -4825,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" @@ -5029,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" @@ -5246,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" @@ -6883,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" @@ -6897,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" @@ -7178,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"