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 2ac1336b1e7f6..ef349e44c4e02 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -822,7 +822,7 @@ function codegenReactiveScope( t.expressionStatement( t.callExpression(t.identifier(detectionFunction), [ t.identifier(loadName), - name, + t.cloneNode(name, true), t.stringLiteral(name.name), t.stringLiteral(cx.fnName), t.stringLiteral("cached"), @@ -833,8 +833,8 @@ function codegenReactiveScope( idempotenceDetectionStatements.push( t.expressionStatement( t.callExpression(t.identifier(detectionFunction), [ - slot, - name, + t.cloneNode(slot, true), + t.cloneNode(name, true), t.stringLiteral(name.name), t.stringLiteral(cx.fnName), t.stringLiteral("recomputed"), @@ -847,6 +847,7 @@ function codegenReactiveScope( ); } const condition = cx.synthesizeName("condition"); + const recomputationBlock = t.cloneNode(computationBlock, true); memoStatement = t.blockStatement([ ...computationBlock.body, t.variableDeclaration("let", [ @@ -863,7 +864,7 @@ function codegenReactiveScope( t.ifStatement( t.identifier(condition), t.blockStatement([ - ...computationBlock.body, + ...recomputationBlock.body, ...idempotenceDetectionStatements, ]) ), diff --git a/fixtures/flight/package.json b/fixtures/flight/package.json index cb0f77c8ea5a9..f07b667dab910 100644 --- a/fixtures/flight/package.json +++ b/fixtures/flight/package.json @@ -10,7 +10,7 @@ "@babel/core": "^7.16.0", "@babel/plugin-proposal-private-property-in-object": "^7.18.6", "@babel/preset-react": "^7.22.5", - "@pmmmwh/react-refresh-webpack-plugin": "0.5.7", + "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", "@svgr/webpack": "^5.5.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", @@ -70,8 +70,8 @@ "predev": "cp -r ../../build/oss-experimental/* ./node_modules/", "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/", "dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"", - "dev:global": "NODE_ENV=development BUILD_PATH=dist node --experimental-loader ./loader/global.js server/global", - "dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server server/region", + "dev:global": "NODE_ENV=development BUILD_PATH=dist node --experimental-loader ./loader/global.js --inspect=127.0.0.1:9230 server/global", + "dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server --inspect=127.0.0.1:9229 server/region", "start": "node scripts/build.js && concurrently \"npm run start:region\" \"npm run start:global\"", "start:global": "NODE_ENV=production node --experimental-loader ./loader/global.js server/global", "start:region": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region", diff --git a/fixtures/flight/playwright.config.js b/fixtures/flight/playwright.config.js index faecd8219acd4..40d86c37c9464 100644 --- a/fixtures/flight/playwright.config.js +++ b/fixtures/flight/playwright.config.js @@ -24,7 +24,7 @@ export default defineConfig({ }, ], webServer: { - command: 'FAST_REFRESH=false yarn dev', + command: 'yarn dev', url: 'http://localhost:3000', reuseExistingServer: !isCI, }, diff --git a/fixtures/flight/yarn.lock b/fixtures/flight/yarn.lock index f007e75761d73..927f680a6ca10 100644 --- a/fixtures/flight/yarn.lock +++ b/fixtures/flight/yarn.lock @@ -2696,19 +2696,17 @@ dependencies: playwright "1.41.2" -"@pmmmwh/react-refresh-webpack-plugin@0.5.7": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz#58f8217ba70069cc6a73f5d7e05e85b458c150e2" - integrity sha512-bcKCAzF0DV2IIROp9ZHkRJa6O4jy7NlnHdWL3GmcUxYWNjLXkK5kfELELwEfSP5hXPfVL/qOGMAROuMQb9GG8Q== +"@pmmmwh/react-refresh-webpack-plugin@0.5.15": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz#f126be97c30b83ed777e2aeabd518bc592e6e7c4" + integrity sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ== dependencies: - ansi-html-community "^0.0.8" - common-path-prefix "^3.0.0" - core-js-pure "^3.8.1" + ansi-html "^0.0.9" + core-js-pure "^3.23.3" error-stack-parser "^2.0.6" - find-up "^5.0.0" html-entities "^2.1.0" - loader-utils "^2.0.0" - schema-utils "^3.0.0" + loader-utils "^2.0.4" + schema-utils "^4.2.0" source-map "^0.7.3" "@sinclair/typebox@^0.24.1": @@ -3323,7 +3321,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv-keywords@^5.0.0: +ajv-keywords@^5.0.0, ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== @@ -3350,6 +3348,16 @@ ajv@^8.0.0, ajv@^8.8.0: require-from-string "^2.0.2" uri-js "^4.2.2" +ajv@^8.9.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" + integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== + dependencies: + fast-deep-equal "^3.1.3" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.4.1" + ansi-escapes@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228" @@ -3363,11 +3371,16 @@ ansi-escapes@^4.3.1: dependencies: type-fest "^0.11.0" -ansi-html-community@0.0.8, ansi-html-community@^0.0.8: +ansi-html-community@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== +ansi-html@^0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.9.tgz#6512d02342ae2cc68131952644a129cb734cd3f0" + integrity sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg== + ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" @@ -3978,11 +3991,6 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -common-path-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" - integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -4057,10 +4065,10 @@ core-js-compat@^3.30.1, core-js-compat@^3.30.2: dependencies: browserslist "^4.21.5" -core-js-pure@^3.8.1: - version "3.36.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.36.0.tgz#ffb34330b14e594d6a9835cf5843b4123f1d95db" - integrity sha512-cN28qmhRNgbMZZMc/RFu5w8pK9VJzpb2rJVR/lHuZJKwmXnoWOpXmMkxqBB514igkp1Hu8WGROsiOAzUcKdHOQ== +core-js-pure@^3.23.3: + version "3.37.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" + integrity sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA== cosmiconfig@^6.0.0: version "6.0.0" @@ -6031,7 +6039,7 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -loader-utils@^2.0.0: +loader-utils@^2.0.0, loader-utils@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== @@ -7705,6 +7713,16 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" +schema-utils@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + semver@7.0.0, semver@~7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -8368,7 +8386,7 @@ update-browserslist-db@^1.0.5: escalade "^3.1.1" picocolors "^1.0.0" -uri-js@^4.2.2: +uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 4ec7de9529548..59af004fb33e4 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -1984,12 +1984,17 @@ export function attach( } if (isRoot) { + // Set supportsStrictMode to false for production renderer builds + const isProductionBuildOfRenderer = renderer.bundleType === 0; + pushOperation(TREE_OPERATION_ADD); pushOperation(id); pushOperation(ElementTypeRoot); pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0); pushOperation(profilingFlags); - pushOperation(StrictModeBits !== 0 ? 1 : 0); + pushOperation( + !isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0, + ); pushOperation(hasOwnerMetadata ? 1 : 0); if (isProfiling) { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css index 1303d009cf01d..156b28f2cadb6 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css @@ -45,12 +45,13 @@ .SelectedComponentName { flex: 1 1 auto; overflow: hidden; - text-overflow: ellipsis; - line-height: normal; + display: flex; + padding: 0.25rem 0; + height: 100%; + align-items: flex-end; } -.Component { - flex: 1 1 auto; +.ComponentName { color: var(--color-component-name); font-family: var(--font-family-monospace); font-size: var(--font-size-monospace-normal); @@ -60,6 +61,10 @@ max-width: 100%; } +.StrictModeNonCompliantComponentName { + color: var(--color-console-error-icon); +} + .Loading { padding: 0.25rem; color: var(--color-dimmer); @@ -68,6 +73,7 @@ } .StrictModeNonCompliant { - margin-right: 0.25rem; + display: inline-flex; + padding: 0.25rem; color: var(--color-console-error-icon); -} \ No newline at end of file +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index 1f1f538b6b633..5722f13f2358f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -251,8 +251,8 @@ export default function InspectedElementWrapper(_: Props): React.Node {
{element.displayName} diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 79e05e9e0d89c..62e0a17af06a4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -1766,9 +1766,9 @@ describe('ReactDOMFizzServer', () => { // Intentionally trigger a key warning here. return (
- {children.map(t => ( - {t} - ))} + {children.map(function mapper(t) { + return {t}; + })}
); } @@ -1813,11 +1813,15 @@ describe('ReactDOMFizzServer', () => { '<%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s', 'inCorrectTag', '\n' + - ' in inCorrectTag (at **)\n' + - ' in C (at **)\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in A (at **)', + (gate(flags => flags.enableOwnerStacks) + ? ' in inCorrectTag (at **)\n' + + ' in C (at **)\n' + + ' in A (at **)' + : ' in inCorrectTag (at **)\n' + + ' in C (at **)\n' + + ' in Suspense (at **)\n' + + ' in div (at **)\n' + + ' in A (at **)'), ); mockError.mockClear(); } else { @@ -1833,22 +1837,19 @@ describe('ReactDOMFizzServer', () => { expect(mockError).toHaveBeenCalledWith( 'Each child in a list should have a unique "key" prop.%s%s' + ' See https://react.dev/link/warning-keys for more information.%s', - gate(flags => flags.enableOwnerStacks) - ? // We currently don't track owners in Fizz which is responsible for this frame. - '' - : '\n\nCheck the top-level render call using
.', + '\n\nCheck the render method of `B`.', '', '\n' + - ' in span (at **)\n' + - // TODO: Because this validates after the div has been mounted, it is part of - // the parent stack but since owner stacks will switch to owners this goes away again. (gate(flags => flags.enableOwnerStacks) - ? ' in div (at **)\n' - : '') + - ' in B (at **)\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in A (at **)', + ? ' in span (at **)\n' + + ' in mapper (at **)\n' + + ' in B (at **)\n' + + ' in A (at **)' + : ' in span (at **)\n' + + ' in B (at **)\n' + + ' in Suspense (at **)\n' + + ' in div (at **)\n' + + ' in A (at **)'), ); } else { expect(mockError).not.toHaveBeenCalled(); @@ -6519,24 +6520,25 @@ describe('ReactDOMFizzServer', () => { mockError(...args.map(normalizeCodeLocInfo)); }; + function App() { + return ( + + + + + + + + ); + } + try { await act(async () => { - const {pipe} = renderToPipeableStream( - - - - - - - , - ); + const {pipe} = renderToPipeableStream(); pipe(writable); }); @@ -6545,17 +6547,29 @@ describe('ReactDOMFizzServer', () => { expect(mockError.mock.calls[0]).toEqual([ 'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s', 'a number for children', - componentStack(['script', 'body', 'html']), + componentStack( + gate(flags => flags.enableOwnerStacks) + ? ['script', 'App'] + : ['script', 'body', 'html', 'App'], + ), ]); expect(mockError.mock.calls[1]).toEqual([ 'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s', 'an array for children', - componentStack(['script', 'body', 'html']), + componentStack( + gate(flags => flags.enableOwnerStacks) + ? ['script', 'App'] + : ['script', 'body', 'html', 'App'], + ), ]); expect(mockError.mock.calls[2]).toEqual([ 'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s', 'something unexpected for children', - componentStack(['script', 'body', 'html']), + componentStack( + gate(flags => flags.enableOwnerStacks) + ? ['script', 'App'] + : ['script', 'body', 'html', 'App'], + ), ]); } else { expect(mockError.mock.calls.length).toBe(0); @@ -8148,4 +8162,91 @@ describe('ReactDOMFizzServer', () => { expect(document.body.textContent).toBe('HelloWorld'); }); + + // @gate __DEV__ && enableOwnerStacks + it('can get the component owner stacks during rendering in dev', async () => { + let stack; + + function Foo() { + return ; + } + function Bar() { + return ( +
+ +
+ ); + } + function Baz() { + stack = React.captureOwnerStack(); + return hi; + } + + await act(() => { + const {pipe} = renderToPipeableStream( +
+ +
, + ); + pipe(writable); + }); + + expect(normalizeCodeLocInfo(stack)).toBe( + '\n in Bar (at **)' + '\n in Foo (at **)', + ); + }); + + // @gate __DEV__ && enableOwnerStacks + it('can get the component owner stacks for onError in dev', async () => { + const thrownError = new Error('hi'); + let caughtError; + let parentStack; + let ownerStack; + + function Foo() { + return ; + } + function Bar() { + return ( +
+ +
+ ); + } + function Baz() { + throw thrownError; + } + + await expect(async () => { + await act(() => { + const {pipe} = renderToPipeableStream( +
+ +
, + { + onError(error, errorInfo) { + caughtError = error; + parentStack = errorInfo.componentStack; + ownerStack = React.captureOwnerStack + ? React.captureOwnerStack() + : null; + }, + }, + ); + pipe(writable); + }); + }).rejects.toThrow(thrownError); + + expect(caughtError).toBe(thrownError); + expect(normalizeCodeLocInfo(parentStack)).toBe( + '\n in Baz (at **)' + + '\n in div (at **)' + + '\n in Bar (at **)' + + '\n in Foo (at **)' + + '\n in div (at **)', + ); + expect(normalizeCodeLocInfo(ownerStack)).toBe( + '\n in Bar (at **)' + '\n in Foo (at **)', + ); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index a41488600655d..065f7cadd7f85 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -835,21 +835,30 @@ describe('ReactDOMServer', () => { expect(() => ReactDOMServer.renderToString()).toErrorDev([ 'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + - ' in span (at **)\n' + - ' in b (at **)\n' + - ' in C (at **)\n' + - ' in font (at **)\n' + - ' in B (at **)\n' + - ' in Child (at **)\n' + - ' in span (at **)\n' + - ' in div (at **)\n' + - ' in App (at **)', + (gate(flags => flags.enableOwnerStacks) + ? ' in span (at **)\n' + + ' in B (at **)\n' + + ' in Child (at **)\n' + + ' in App (at **)' + : ' in span (at **)\n' + + ' in b (at **)\n' + + ' in C (at **)\n' + + ' in font (at **)\n' + + ' in B (at **)\n' + + ' in Child (at **)\n' + + ' in span (at **)\n' + + ' in div (at **)\n' + + ' in App (at **)'), 'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + - ' in span (at **)\n' + - ' in Child (at **)\n' + - ' in span (at **)\n' + - ' in div (at **)\n' + - ' in App (at **)', + (gate(flags => flags.enableOwnerStacks) + ? ' in span (at **)\n' + + ' in Child (at **)\n' + + ' in App (at **)' + : ' in span (at **)\n' + + ' in Child (at **)\n' + + ' in span (at **)\n' + + ' in div (at **)\n' + + ' in App (at **)'), ]); }); @@ -885,9 +894,11 @@ describe('ReactDOMServer', () => { expect(() => ReactDOMServer.renderToString()).toErrorDev([ // ReactDOMServer(App > div > span) 'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + - ' in span (at **)\n' + - ' in div (at **)\n' + - ' in App (at **)', + (gate(flags => flags.enableOwnerStacks) + ? ' in span (at **)\n' + ' in App (at **)' + : ' in span (at **)\n' + + ' in div (at **)\n' + + ' in App (at **)'), // ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink) 'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in blink (at **)', @@ -898,15 +909,21 @@ describe('ReactDOMServer', () => { ' in App2 (at **)', // ReactDOMServer(App > div > Child > span) 'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + - ' in span (at **)\n' + - ' in Child (at **)\n' + - ' in div (at **)\n' + - ' in App (at **)', + (gate(flags => flags.enableOwnerStacks) + ? ' in span (at **)\n' + + ' in Child (at **)\n' + + ' in App (at **)' + : ' in span (at **)\n' + + ' in Child (at **)\n' + + ' in div (at **)\n' + + ' in App (at **)'), // ReactDOMServer(App > div > font) 'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + - ' in font (at **)\n' + - ' in div (at **)\n' + - ' in App (at **)', + (gate(flags => flags.enableOwnerStacks) + ? ' in font (at **)\n' + ' in App (at **)' + : ' in font (at **)\n' + + ' in div (at **)\n' + + ' in App (at **)'), ]); }); diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 8e58b5c44aa84..beee88de2549b 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -36,6 +36,7 @@ import type { Transition, } from './ReactFiberTracingMarkerComponent'; import type {ConcurrentUpdate} from './ReactFiberConcurrentUpdates'; +import type {ComponentStackNode} from 'react-server/src/ReactFizzComponentStack'; // Unwind Circular: moved from ReactFiberHooks.old export type HookType = @@ -439,5 +440,5 @@ export type Dispatcher = { export type AsyncDispatcher = { getCacheForType: (resourceType: () => T) => T, // DEV-only (or !disableStringRefs) - getOwner: () => null | Fiber | ReactComponentInfo, + getOwner: () => null | Fiber | ReactComponentInfo | ComponentStackNode, }; diff --git a/packages/react-server/src/ReactFizzAsyncDispatcher.js b/packages/react-server/src/ReactFizzAsyncDispatcher.js index 8e8cb2e219992..3a548ae138039 100644 --- a/packages/react-server/src/ReactFizzAsyncDispatcher.js +++ b/packages/react-server/src/ReactFizzAsyncDispatcher.js @@ -8,9 +8,12 @@ */ import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes'; +import type {ComponentStackNode} from './ReactFizzComponentStack'; import {disableStringRefs} from 'shared/ReactFeatureFlags'; +import {currentTaskInDEV} from './ReactFizzCurrentTask'; + function getCacheForType(resourceType: () => T): T { throw new Error('Not implemented.'); } @@ -19,8 +22,14 @@ export const DefaultAsyncDispatcher: AsyncDispatcher = ({ getCacheForType, }: any); -if (__DEV__ || !disableStringRefs) { - // Fizz never tracks owner but the JSX runtime looks for this. +if (__DEV__) { + DefaultAsyncDispatcher.getOwner = (): ComponentStackNode | null => { + if (currentTaskInDEV === null) { + return null; + } + return currentTaskInDEV.componentStack; + }; +} else if (!disableStringRefs) { DefaultAsyncDispatcher.getOwner = (): null => { return null; }; diff --git a/packages/react-server/src/ReactFizzCallUserSpace.js b/packages/react-server/src/ReactFizzCallUserSpace.js new file mode 100644 index 0000000000000..4a376607dc6df --- /dev/null +++ b/packages/react-server/src/ReactFizzCallUserSpace.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {LazyComponent} from 'react/src/ReactLazy'; + +// These indirections exists so we can exclude its stack frame in DEV (and anything below it). +// TODO: Consider marking the whole bundle instead of these boundaries. + +/** @noinline */ +export function callComponentInDEV( + Component: (p: Props, arg: Arg) => R, + props: Props, + secondArg: Arg, +): R { + return Component(props, secondArg); +} + +interface ClassInstance { + render(): R; +} + +/** @noinline */ +export function callRenderInDEV(instance: ClassInstance): R { + return instance.render(); +} + +/** @noinline */ +export function callLazyInitInDEV(lazy: LazyComponent): any { + const payload = lazy._payload; + const init = lazy._init; + return init(payload); +} diff --git a/packages/react-server/src/ReactFizzComponentStack.js b/packages/react-server/src/ReactFizzComponentStack.js index 84b4d82d8d45f..a16b2c5f91351 100644 --- a/packages/react-server/src/ReactFizzComponentStack.js +++ b/packages/react-server/src/ReactFizzComponentStack.js @@ -7,27 +7,39 @@ * @flow */ +import type {ReactComponentInfo} from 'shared/ReactTypes'; + import { describeBuiltInComponentFrame, describeFunctionComponentFrame, describeClassComponentFrame, } from 'shared/ReactComponentStackFrame'; +import {enableOwnerStacks} from 'shared/ReactFeatureFlags'; + +import {formatOwnerStack} from './ReactFizzOwnerStack'; + // DEV-only reverse linked list representing the current component stack type BuiltInComponentStackNode = { tag: 0, parent: null | ComponentStackNode, type: string, + owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack?: null | string | Error, // DEV only }; type FunctionComponentStackNode = { tag: 1, parent: null | ComponentStackNode, type: Function, + owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack?: null | string | Error, // DEV only }; type ClassComponentStackNode = { tag: 2, parent: null | ComponentStackNode, type: Function, + owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack?: null | string | Error, // DEV only }; export type ComponentStackNode = | BuiltInComponentStackNode @@ -60,3 +72,82 @@ export function getStackByComponentStackNode( return '\nError generating stack: ' + x.message + '\n' + x.stack; } } + +function describeFunctionComponentFrameWithoutLineNumber(fn: Function): string { + // We use this because we don't actually want to describe the line of the component + // but just the component name. + const name = fn ? fn.displayName || fn.name : ''; + return name ? describeBuiltInComponentFrame(name) : ''; +} + +export function getOwnerStackByComponentStackNodeInDev( + componentStack: ComponentStackNode, +): string { + if (!enableOwnerStacks || !__DEV__) { + return ''; + } + try { + let info = ''; + + // The owner stack of the current component will be where it was created, i.e. inside its owner. + // There's no actual name of the currently executing component. Instead, that is available + // on the regular stack that's currently executing. However, for built-ins there is no such + // named stack frame and it would be ignored as being internal anyway. Therefore we add + // add one extra frame just to describe the "current" built-in component by name. + // Similarly, if there is no owner at all, then there's no stack frame so we add the name + // of the root component to the stack to know which component is currently executing. + switch (componentStack.tag) { + case 0: + info += describeBuiltInComponentFrame(componentStack.type); + break; + case 1: + case 2: + if (!componentStack.owner) { + // Only if we have no other data about the callsite do we add + // the component name as the single stack frame. + info += describeFunctionComponentFrameWithoutLineNumber( + componentStack.type, + ); + } + break; + } + + let owner: void | null | ComponentStackNode | ReactComponentInfo = + componentStack; + + while (owner) { + if (typeof owner.tag === 'number') { + const node: ComponentStackNode = (owner: any); + owner = node.owner; + let debugStack = node.stack; + // If we don't actually print the stack if there is no owner of this JSX element. + // In a real app it's typically not useful since the root app is always controlled + // by the framework. These also tend to have noisy stacks because they're not rooted + // in a React render but in some imperative bootstrapping code. It could be useful + // if the element was created in module scope. E.g. hoisted. We could add a a single + // stack frame for context for example but it doesn't say much if that's a wrapper. + if (owner && debugStack) { + if (typeof debugStack !== 'string') { + // Stash the formatted stack so that we can avoid redoing the filtering. + node.stack = debugStack = formatOwnerStack(debugStack); + } + if (debugStack !== '') { + info += '\n' + debugStack; + } + } + } else if (typeof owner.stack === 'string') { + // Server Component + if (owner.stack !== '') { + info += '\n' + owner.stack; + } + const componentInfo: ReactComponentInfo = (owner: any); + owner = componentInfo.owner; + } else { + break; + } + } + return info; + } catch (x) { + return '\nError generating stack: ' + x.message + '\n' + x.stack; + } +} diff --git a/packages/react-server/src/ReactFizzCurrentTask.js b/packages/react-server/src/ReactFizzCurrentTask.js new file mode 100644 index 0000000000000..3bd62f0a5cde0 --- /dev/null +++ b/packages/react-server/src/ReactFizzCurrentTask.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Task} from './ReactFizzServer'; + +// DEV-only global reference to the currently executing task +export let currentTaskInDEV: null | Task = null; + +export function setCurrentTaskInDEV(task: null | Task): void { + if (__DEV__) { + currentTaskInDEV = task; + } +} diff --git a/packages/react-server/src/ReactFizzOwnerStack.js b/packages/react-server/src/ReactFizzOwnerStack.js new file mode 100644 index 0000000000000..8d6b5d94fd0db --- /dev/null +++ b/packages/react-server/src/ReactFizzOwnerStack.js @@ -0,0 +1,117 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; + +import { + callLazyInitInDEV, + callComponentInDEV, + callRenderInDEV, +} from './ReactFizzCallUserSpace'; + +// TODO: Make this configurable on the root. +const externalRegExp = /\/node\_modules\/|\(\\)/; + +let callComponentFrame: null | string = null; +let callIteratorFrame: null | string = null; +let callLazyInitFrame: null | string = null; + +function isNotExternal(stackFrame: string): boolean { + return !externalRegExp.test(stackFrame); +} + +function initCallComponentFrame(): string { + // Extract the stack frame of the callComponentInDEV function. + const error = callComponentInDEV(Error, 'react-stack-top-frame', {}); + const stack = error.stack; + const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; + const endIdx = stack.indexOf('\n', startIdx); + if (endIdx === -1) { + return stack.slice(startIdx); + } + return stack.slice(startIdx, endIdx); +} + +function initCallRenderFrame(): string { + // Extract the stack frame of the callRenderInDEV function. + try { + (callRenderInDEV: any)({render: null}); + return ''; + } catch (error) { + const stack = error.stack; + const startIdx = stack.startsWith('TypeError: ') + ? stack.indexOf('\n') + 1 + : 0; + const endIdx = stack.indexOf('\n', startIdx); + if (endIdx === -1) { + return stack.slice(startIdx); + } + return stack.slice(startIdx, endIdx); + } +} + +function initCallLazyInitFrame(): string { + // Extract the stack frame of the callLazyInitInDEV function. + const error = callLazyInitInDEV({ + $$typeof: REACT_LAZY_TYPE, + _init: Error, + _payload: 'react-stack-top-frame', + }); + const stack = error.stack; + const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; + const endIdx = stack.indexOf('\n', startIdx); + if (endIdx === -1) { + return stack.slice(startIdx); + } + return stack.slice(startIdx, endIdx); +} + +function filterDebugStack(error: Error): string { + // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly + // to save bandwidth even in DEV. We'll also replay these stacks on the client so by + // stripping them early we avoid that overhead. Otherwise we'd normally just rely on + // the DevTools or framework's ignore lists to filter them out. + let stack = error.stack; + if (stack.startsWith('Error: react-stack-top-frame\n')) { + // V8's default formatting prefixes with the error message which we + // don't want/need. + stack = stack.slice(29); + } + const frames = stack.split('\n').slice(1); + if (callComponentFrame === null) { + callComponentFrame = initCallComponentFrame(); + } + let lastFrameIdx = frames.indexOf(callComponentFrame); + if (lastFrameIdx === -1) { + if (callLazyInitFrame === null) { + callLazyInitFrame = initCallLazyInitFrame(); + } + lastFrameIdx = frames.indexOf(callLazyInitFrame); + if (lastFrameIdx === -1) { + if (callIteratorFrame === null) { + callIteratorFrame = initCallRenderFrame(); + } + lastFrameIdx = frames.indexOf(callIteratorFrame); + } + } + if (lastFrameIdx !== -1) { + // Cut off everything after our "callComponent" slot since it'll be Fiber internals. + frames.length = lastFrameIdx; + } else { + // We didn't find any internal callsite out to user space. + // This means that this was called outside an owner or the owner is fully internal. + // To keep things light we exclude the entire trace in this case. + return ''; + } + return frames.filter(isNotExternal).join('\n'); +} + +export function formatOwnerStack(ownerStackTrace: Error): string { + return filterDebugStack(ownerStackTrace); +} diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 75b195e1e25fb..6c21a1828da58 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -20,6 +20,7 @@ import type { Wakeable, Thenable, ReactFormState, + ReactComponentInfo, } from 'shared/ReactTypes'; import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type { @@ -113,8 +114,17 @@ import { getActionStateMatchingIndex, } from './ReactFizzHooks'; import {DefaultAsyncDispatcher} from './ReactFizzAsyncDispatcher'; -import {getStackByComponentStackNode} from './ReactFizzComponentStack'; +import { + getStackByComponentStackNode, + getOwnerStackByComponentStackNodeInDev, +} from './ReactFizzComponentStack'; import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext'; +import {currentTaskInDEV, setCurrentTaskInDEV} from './ReactFizzCurrentTask'; +import { + callLazyInitInDEV, + callComponentInDEV, + callRenderInDEV, +} from './ReactFizzCallUserSpace'; import { getIteratorFn, @@ -150,6 +160,7 @@ import { disableDefaultPropsExceptForClasses, enableAsyncIterableChildren, disableStringRefs, + enableOwnerStacks, } from 'shared/ReactFeatureFlags'; import assign from 'shared/assign'; @@ -789,14 +800,16 @@ function createPendingSegment( }; } -// DEV-only global reference to the currently executing task -let currentTaskInDEV: null | Task = null; function getCurrentStackInDEV(): string { if (__DEV__) { if (currentTaskInDEV === null || currentTaskInDEV.componentStack === null) { return ''; } - // TODO: Support owner based stacks for logs during SSR. + if (enableOwnerStacks) { + return getOwnerStackByComponentStackNodeInDev( + currentTaskInDEV.componentStack, + ); + } return getStackByComponentStackNode(currentTaskInDEV.componentStack); } return ''; @@ -809,7 +822,18 @@ function getStackFromNode(stackNode: ComponentStackNode): string { function createBuiltInComponentStack( task: Task, type: string, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): ComponentStackNode { + if (__DEV__) { + return { + tag: 0, + parent: task.componentStack, + type, + owner, + stack, + }; + } return { tag: 0, parent: task.componentStack, @@ -819,7 +843,18 @@ function createBuiltInComponentStack( function createFunctionComponentStack( task: Task, type: Function, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): ComponentStackNode { + if (__DEV__) { + return { + tag: 1, + parent: task.componentStack, + type, + owner, + stack, + }; + } return { tag: 1, parent: task.componentStack, @@ -829,7 +864,18 @@ function createFunctionComponentStack( function createClassComponentStack( task: Task, type: Function, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): ComponentStackNode { + if (__DEV__) { + return { + tag: 2, + parent: task.componentStack, + type, + owner, + stack, + }; + } return { tag: 2, parent: task.componentStack, @@ -840,14 +886,16 @@ function createClassComponentStack( function createComponentStackFromType( task: Task, type: Function | string, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): ComponentStackNode { if (typeof type === 'string') { - return createBuiltInComponentStack(task, type); + return createBuiltInComponentStack(task, type, owner, stack); } if (shouldConstruct(type)) { - return createClassComponentStack(task, type); + return createClassComponentStack(task, type, owner, stack); } - return createFunctionComponentStack(task, type); + return createFunctionComponentStack(task, type, owner, stack); } type ThrownInfo = { @@ -966,6 +1014,8 @@ function renderSuspenseBoundary( someTask: Task, keyPath: KeyNode, props: Object, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { if (someTask.replay !== null) { // If we're replaying through this pass, it means we're replaying through @@ -988,7 +1038,7 @@ function renderSuspenseBoundary( // If we end up creating the fallback task we need it to have the correct stack which is // the stack for the boundary itself. We stash it here so we can use it if needed later const suspenseComponentStack = (task.componentStack = - createBuiltInComponentStack(task, 'Suspense')); + createBuiltInComponentStack(task, 'Suspense', owner, stack)); const prevKeyPath = task.keyPath; const parentBoundary = task.blockedBoundary; @@ -1161,12 +1211,14 @@ function replaySuspenseBoundary( childSlots: ResumeSlots, fallbackNodes: Array, fallbackSlots: ResumeSlots, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { const previousComponentStack = task.componentStack; // If we end up creating the fallback task we need it to have the correct stack which is // the stack for the boundary itself. We stash it here so we can use it if needed later const suspenseComponentStack = (task.componentStack = - createBuiltInComponentStack(task, 'Suspense')); + createBuiltInComponentStack(task, 'Suspense', owner, stack)); const prevKeyPath = task.keyPath; const previousReplaySet: ReplaySet = task.replay; @@ -1294,9 +1346,16 @@ function renderBackupSuspenseBoundary( task: Task, keyPath: KeyNode, props: Object, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ) { const previousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack(task, 'Suspense'); + task.componentStack = createBuiltInComponentStack( + task, + 'Suspense', + owner, + stack, + ); const content = props.children; const segment = task.blockedSegment; @@ -1321,9 +1380,11 @@ function renderHostElement( keyPath: KeyNode, type: string, props: Object, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { const previousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack(task, type); + task.componentStack = createBuiltInComponentStack(task, type, owner, stack); const segment = task.blockedSegment; if (segment === null) { // Replay @@ -1405,7 +1466,12 @@ function renderWithHooks( componentIdentity, prevThenableState, ); - const result = Component(props, secondArg); + let result; + if (__DEV__) { + result = callComponentInDEV(Component, props, secondArg); + } else { + result = Component(props, secondArg); + } return finishHooks(Component, props, result, secondArg); } @@ -1417,7 +1483,12 @@ function finishClassComponent( Component: any, props: any, ): ReactNodeList { - const nextChildren = instance.render(); + let nextChildren; + if (__DEV__) { + nextChildren = callRenderInDEV(instance); + } else { + nextChildren = instance.render(); + } if (__DEV__) { if (instance.props !== props) { @@ -1503,10 +1574,17 @@ function renderClassComponent( keyPath: KeyNode, Component: any, props: any, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { const resolvedProps = resolveClassComponentProps(Component, props); const previousComponentStack = task.componentStack; - task.componentStack = createClassComponentStack(task, Component); + task.componentStack = createClassComponentStack( + task, + Component, + owner, + stack, + ); const maskedContext = !disableLegacyContext ? getMaskedContext(Component, task.legacyContext) : undefined; @@ -1541,13 +1619,20 @@ function renderFunctionComponent( keyPath: KeyNode, Component: any, props: any, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { let legacyContext; if (!disableLegacyContext) { legacyContext = getMaskedContext(Component, task.legacyContext); } const previousComponentStack = task.componentStack; - task.componentStack = createFunctionComponentStack(task, Component); + task.componentStack = createFunctionComponentStack( + task, + Component, + owner, + stack, + ); if (__DEV__) { if ( @@ -1750,9 +1835,16 @@ function renderForwardRef( type: any, props: Object, ref: any, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { const previousComponentStack = task.componentStack; - task.componentStack = createFunctionComponentStack(task, type.render); + task.componentStack = createFunctionComponentStack( + task, + type.render, + owner, + stack, + ); let propsWithoutRef; if (enableRefAsProp && 'ref' in props) { @@ -1802,13 +1894,24 @@ function renderMemo( type: any, props: Object, ref: any, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { const innerType = type.type; const resolvedProps = resolveDefaultPropsOnNonClassComponent( innerType, props, ); - renderElement(request, task, keyPath, innerType, resolvedProps, ref); + renderElement( + request, + task, + keyPath, + innerType, + resolvedProps, + ref, + owner, + stack, + ); } function renderContextConsumer( @@ -1875,17 +1978,33 @@ function renderLazyComponent( lazyComponent: LazyComponentType, props: Object, ref: any, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { const previousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack(task, 'Lazy'); - const payload = lazyComponent._payload; - const init = lazyComponent._init; - const Component = init(payload); + task.componentStack = createBuiltInComponentStack(task, 'Lazy', owner, stack); + let Component; + if (__DEV__) { + Component = callLazyInitInDEV(lazyComponent); + } else { + const payload = lazyComponent._payload; + const init = lazyComponent._init; + Component = init(payload); + } const resolvedProps = resolveDefaultPropsOnNonClassComponent( Component, props, ); - renderElement(request, task, keyPath, Component, resolvedProps, ref); + renderElement( + request, + task, + keyPath, + Component, + resolvedProps, + ref, + owner, + stack, + ); task.componentStack = previousComponentStack; } @@ -1916,18 +2035,28 @@ function renderElement( type: any, props: Object, ref: any, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { if (typeof type === 'function') { if (shouldConstruct(type)) { - renderClassComponent(request, task, keyPath, type, props); + renderClassComponent(request, task, keyPath, type, props, owner, stack); return; } else { - renderFunctionComponent(request, task, keyPath, type, props); + renderFunctionComponent( + request, + task, + keyPath, + type, + props, + owner, + stack, + ); return; } } if (typeof type === 'string') { - renderHostElement(request, task, keyPath, type, props); + renderHostElement(request, task, keyPath, type, props, owner, stack); return; } @@ -1958,7 +2087,12 @@ function renderElement( } case REACT_SUSPENSE_LIST_TYPE: { const preiousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack(task, 'SuspenseList'); + task.componentStack = createBuiltInComponentStack( + task, + 'SuspenseList', + owner, + stack, + ); // TODO: SuspenseList should control the boundaries. const prevKeyPath = task.keyPath; task.keyPath = keyPath; @@ -1982,9 +2116,16 @@ function renderElement( enableSuspenseAvoidThisFallbackFizz && props.unstable_avoidThisFallback === true ) { - renderBackupSuspenseBoundary(request, task, keyPath, props); + renderBackupSuspenseBoundary( + request, + task, + keyPath, + props, + owner, + stack, + ); } else { - renderSuspenseBoundary(request, task, keyPath, props); + renderSuspenseBoundary(request, task, keyPath, props, owner, stack); } return; } @@ -1993,11 +2134,20 @@ function renderElement( if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { case REACT_FORWARD_REF_TYPE: { - renderForwardRef(request, task, keyPath, type, props, ref); + renderForwardRef( + request, + task, + keyPath, + type, + props, + ref, + owner, + stack, + ); return; } case REACT_MEMO_TYPE: { - renderMemo(request, task, keyPath, type, props, ref); + renderMemo(request, task, keyPath, type, props, ref, owner, stack); return; } case REACT_PROVIDER_TYPE: { @@ -2034,7 +2184,16 @@ function renderElement( // Fall through } case REACT_LAZY_TYPE: { - renderLazyComponent(request, task, keyPath, type, props); + renderLazyComponent( + request, + task, + keyPath, + type, + props, + ref, + owner, + stack, + ); return; } } @@ -2114,6 +2273,8 @@ function replayElement( props: Object, ref: any, replay: ReplaySet, + owner: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack: null | Error, // DEV only ): void { // We're replaying. Find the path to follow. const replayNodes = replay.nodes; @@ -2141,7 +2302,7 @@ function replayElement( const currentNode = task.node; task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1}; try { - renderElement(request, task, keyPath, type, props, ref); + renderElement(request, task, keyPath, type, props, ref, owner, stack); if ( task.replay.pendingTasks === 1 && task.replay.nodes.length > 0 @@ -2207,6 +2368,8 @@ function replayElement( node[3], node[4] === null ? [] : node[4][2], node[4] === null ? null : node[4][3], + owner, + stack, ); } // We finished rendering this node, so now we can consume this @@ -2367,11 +2530,37 @@ function renderNodeDestructive( ref = element.ref; } + const owner = __DEV__ ? element._owner : null; + const stack = __DEV__ && enableOwnerStacks ? element._debugStack : null; + const name = getComponentNameFromType(type); const keyOrIndex = key == null ? (childIndex === -1 ? 0 : childIndex) : key; const keyPath = [task.keyPath, name, keyOrIndex]; if (task.replay !== null) { + if (__DEV__ && enableOwnerStacks) { + const debugTask: null | ConsoleTask = element._debugTask; + if (debugTask) { + debugTask.run( + replayElement.bind( + null, + request, + task, + keyPath, + name, + keyOrIndex, + childIndex, + type, + props, + ref, + task.replay, + owner, + stack, + ), + ); + return; + } + } replayElement( request, task, @@ -2383,12 +2572,33 @@ function renderNodeDestructive( props, ref, task.replay, + owner, + stack, ); // No matches found for this node. We assume it's already emitted in the // prelude and skip it during the replay. } else { // We're doing a plain render. - renderElement(request, task, keyPath, type, props, ref); + if (__DEV__ && enableOwnerStacks) { + const debugTask: null | ConsoleTask = element._debugTask; + if (debugTask) { + debugTask.run( + renderElement.bind( + null, + request, + task, + keyPath, + type, + props, + ref, + owner, + stack, + ), + ); + return; + } + } + renderElement(request, task, keyPath, type, props, ref, owner, stack); } return; } @@ -2399,11 +2609,21 @@ function renderNodeDestructive( ); case REACT_LAZY_TYPE: { const previousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack(task, 'Lazy'); + task.componentStack = createBuiltInComponentStack( + task, + 'Lazy', + null, + null, + ); const lazyNode: LazyComponentType = (node: any); - const payload = lazyNode._payload; - const init = lazyNode._init; - const resolvedNode = init(payload); + let resolvedNode; + if (__DEV__) { + resolvedNode = callLazyInitInDEV(lazyNode); + } else { + const payload = lazyNode._payload; + const init = lazyNode._init; + resolvedNode = init(payload); + } // We restore the stack before rendering the resolved node because once the Lazy // has resolved any future errors @@ -2465,6 +2685,8 @@ function renderNodeDestructive( task.componentStack = createBuiltInComponentStack( task, 'AsyncIterable', + null, + null, ); // Restore the thenable state before resuming. @@ -2700,14 +2922,54 @@ function warnForMissingKey(request: Request, task: Task, child: mixed): void { } didWarnForKey.add(parentStackFrame); + const componentName = getComponentNameFromType(child.type); + const childOwner = child._owner; + const parentOwner = parentStackFrame.owner; + + let currentComponentErrorInfo = ''; + if (parentOwner && typeof parentOwner.tag === 'number') { + const name = getComponentNameFromType((parentOwner: any).type); + if (name) { + currentComponentErrorInfo = + '\n\nCheck the render method of `' + name + '`.'; + } + } + if (!currentComponentErrorInfo) { + if (componentName) { + currentComponentErrorInfo = `\n\nCheck the top-level render call using <${componentName}>.`; + } + } + + // Usually the current owner is the offender, but if it accepts children as a + // property, it may be the creator of the child that's responsible for + // assigning it a key. + let childOwnerAppendix = ''; + if (childOwner != null && parentOwner !== childOwner) { + let ownerName = null; + if (typeof childOwner.tag === 'number') { + ownerName = getComponentNameFromType((childOwner: any).type); + } else if (typeof childOwner.name === 'string') { + ownerName = childOwner.name; + } + if (ownerName) { + // Give the component that originally created this child. + childOwnerAppendix = ` It was passed a child from ${ownerName}.`; + } + } + // We create a fake component stack for the child to log the stack trace from. - const stackFrame = createComponentStackFromType(task, (child: any).type); + const stackFrame = createComponentStackFromType( + task, + (child: any).type, + (child: any)._owner, + enableOwnerStacks ? (child: any)._debugStack : null, + ); task.componentStack = stackFrame; console.error( 'Each child in a list should have a unique "key" prop.' + '%s%s See https://react.dev/link/warning-keys for more information.', - '', - '', + currentComponentErrorInfo, + childOwnerAppendix, ); task.componentStack = stackFrame.parent; } @@ -3736,7 +3998,7 @@ function retryRenderTask( let prevTaskInDEV = null; if (__DEV__) { prevTaskInDEV = currentTaskInDEV; - currentTaskInDEV = task; + setCurrentTaskInDEV(task); } const childrenLength = segment.children.length; @@ -3813,7 +4075,7 @@ function retryRenderTask( return; } finally { if (__DEV__) { - currentTaskInDEV = prevTaskInDEV; + setCurrentTaskInDEV(prevTaskInDEV); } } } @@ -3831,7 +4093,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void { let prevTaskInDEV = null; if (__DEV__) { prevTaskInDEV = currentTaskInDEV; - currentTaskInDEV = task; + setCurrentTaskInDEV(task); } try { @@ -3900,7 +4162,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void { return; } finally { if (__DEV__) { - currentTaskInDEV = prevTaskInDEV; + setCurrentTaskInDEV(prevTaskInDEV); } } } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 7ff1e47897299..cb0d5ee395232 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -168,7 +168,7 @@ function getStack(error: Error): string { function initCallComponentFrame(): string { // Extract the stack frame of the callComponentInDEV function. - const error = callComponentInDEV(Error, 'react-stack-top-frame', {}); + const error = callComponentInDEV(Error, 'react-stack-top-frame', {}, null); const stack = getStack(error); const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; const endIdx = stack.indexOf('\n', startIdx); @@ -991,6 +991,7 @@ function callComponentInDEV( Component: (p: Props, arg: void) => R, props: Props, componentDebugInfo: ReactComponentInfo, + debugTask: null | ConsoleTask, ): R { // The secondArg is always undefined in Server Components since refs error early. const secondArg = undefined; @@ -998,6 +999,18 @@ function callComponentInDEV( try { if (supportsComponentStorage) { // Run the component in an Async Context that tracks the current owner. + if (enableOwnerStacks && debugTask) { + return debugTask.run( + // $FlowFixMe[method-unbinding] + componentStorage.run.bind( + componentStorage, + componentDebugInfo, + Component, + props, + secondArg, + ), + ); + } return componentStorage.run( componentDebugInfo, Component, @@ -1005,6 +1018,9 @@ function callComponentInDEV( secondArg, ); } else { + if (enableOwnerStacks && debugTask) { + return debugTask.run(Component.bind(null, props, secondArg)); + } return Component(props, secondArg); } } finally { @@ -1028,6 +1044,7 @@ function renderFunctionComponent( props: Props, owner: null | ReactComponentInfo, // DEV-only stack: null | string, // DEV-only + debugTask: null | ConsoleTask, // DEV-only validated: number, // DEV-only ): ReactJSONValue { // Reset the task's thenable state before continuing, so that if a later @@ -1075,11 +1092,22 @@ function renderFunctionComponent( task.environmentName = componentEnv; if (enableOwnerStacks) { - warnForMissingKey(request, key, validated, componentDebugInfo); + warnForMissingKey( + request, + key, + validated, + componentDebugInfo, + debugTask, + ); } } prepareToUseHooksForComponent(prevThenableState, componentDebugInfo); - result = callComponentInDEV(Component, props, componentDebugInfo); + result = callComponentInDEV( + Component, + props, + componentDebugInfo, + debugTask, + ); } else { prepareToUseHooksForComponent(prevThenableState, null); // The secondArg is always undefined in Server Components since refs error early. @@ -1235,6 +1263,7 @@ function warnForMissingKey( key: null | string, validated: number, componentDebugInfo: ReactComponentInfo, + debugTask: null | ConsoleTask, ): void { if (__DEV__) { if (validated !== 2) { @@ -1267,6 +1296,7 @@ function warnForMissingKey( }, null, componentDebugInfo, + debugTask, ); } } @@ -1482,6 +1512,7 @@ function renderElement( props: any, owner: null | ReactComponentInfo, // DEV only stack: null | string, // DEV only + debugTask: null | ConsoleTask, // DEV only validated: number, // DEV only ): ReactJSONValue { if (ref !== null && ref !== undefined) { @@ -1514,6 +1545,7 @@ function renderElement( props, owner, stack, + debugTask, validated, ); } else if (type === REACT_FRAGMENT_TYPE && key === null) { @@ -1562,6 +1594,7 @@ function renderElement( props, owner, stack, + debugTask, validated, ); } @@ -1574,6 +1607,7 @@ function renderElement( props, owner, stack, + debugTask, validated, ); } @@ -1587,6 +1621,7 @@ function renderElement( props, owner, stack, + debugTask, validated, ); } @@ -2190,6 +2225,7 @@ function renderModelDestructive( ? element._debugStack : filterDebugStack(element._debugStack) : null, + __DEV__ && enableOwnerStacks ? element._debugTask : null, __DEV__ && enableOwnerStacks ? element._store.validated : 0, ); if ( diff --git a/scripts/shared/listChangedFiles.js b/scripts/shared/listChangedFiles.js index 2fd80d4b488c4..b6449e5cc6e4b 100644 --- a/scripts/shared/listChangedFiles.js +++ b/scripts/shared/listChangedFiles.js @@ -19,14 +19,46 @@ const exec = (command, args) => { return execFileSync(command, args, options); }; +const isGit = () => { + try { + const wt = execGitCmd(['rev-parse', '--is-inside-work-tree']); + return wt.length > 0 && wt[0] === 'true'; + } catch (_e) { + return false; + } +}; + +const isSl = () => { + try { + execSlCmd(['whereami']); + return true; + } catch (_e) { + return false; + } +}; + const execGitCmd = args => exec('git', args).trim().toString().split('\n'); +const execSlCmd = args => exec('sl', args).trim().toString().split('\n'); const listChangedFiles = () => { - const mergeBase = execGitCmd(['merge-base', 'HEAD', 'main']); - return new Set([ - ...execGitCmd(['diff', '--name-only', '--diff-filter=ACMRTUB', mergeBase]), - ...execGitCmd(['ls-files', '--others', '--exclude-standard']), - ]); + if (isGit()) { + const mergeBase = execGitCmd(['merge-base', 'HEAD', 'main']); + return new Set([ + ...execGitCmd([ + 'diff', + '--name-only', + '--diff-filter=ACMRTUB', + mergeBase, + ]), + ...execGitCmd(['ls-files', '--others', '--exclude-standard']), + ]); + } else if (isSl()) { + const mergeBase = execSlCmd(['log', '-r', 'last(public() & ::.)'])[0] + .trim() + .split(/\s+/)[1]; + return new Set(execSlCmd(['status', '--no-status', '--rev', mergeBase])); + } + throw new Error('Not a git or sl repo'); }; module.exports = listChangedFiles;