Merge 4299e1812d into sapling-pr-archive-mofeiZ

This commit is contained in:
mofeiZ
2025-04-21 18:12:43 -04:00
committed by GitHub
186 changed files with 6340 additions and 2117 deletions
+2
View File
@@ -615,6 +615,8 @@ module.exports = {
GetAnimationsOptions: 'readonly',
Animatable: 'readonly',
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',
-18
View File
@@ -1,18 +0,0 @@
---
name: "⚛React 19 beta issue"
about: Report a issue with React 19 beta.
title: '[React 19]'
labels: 'React 19'
---
## Summary
<!--
Please provide a CodeSandbox (https://codesandbox.io/s/new), a link to a
repository on GitHub, or provide a minimal code example that reproduces the
problem. You may provide a screenshot of the application if you think it is
relevant to your bug report. Here are some tips for providing a minimal
example: https://stackoverflow.com/help/mcve.
-->
@@ -10,7 +10,19 @@ on:
permissions: {}
jobs:
check_access:
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
+4 -1
View File
@@ -16,6 +16,9 @@ on:
version_name:
required: true
type: string
tag_version:
required: false
type: string
secrets:
NPM_TOKEN:
required: true
@@ -55,4 +58,4 @@ jobs:
- name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag ${{ inputs.dist_tag }}
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
@@ -14,6 +14,9 @@ on:
version_name:
required: true
type: string
tag_version:
required: false
type: string
permissions: {}
@@ -29,5 +32,6 @@ jobs:
release_channel: ${{ inputs.release_channel }}
dist_tag: ${{ inputs.dist_tag }}
version_name: ${{ inputs.version_name }}
tag_version: ${{ inputs.tag_version }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,23 +0,0 @@
name: (Compiler) Publish Prereleases Weekly
on:
schedule:
# At 10 minutes past 9:00 on Mon
- cron: 10 9 * * 1
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
publish_prerelease_beta:
name: Publish to beta channel
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
with:
commit_sha: ${{ github.sha }}
release_channel: beta
dist_tag: beta
version_name: '19.0.0'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -10,7 +10,19 @@ on:
permissions: {}
jobs:
check_access:
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
+15
View File
@@ -13,7 +13,14 @@ on:
dist_tag:
required: true
type: string
enableFailureNotification:
description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
required: false
type: boolean
secrets:
DISCORD_WEBHOOK_URL:
description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
required: false
GH_TOKEN:
required: true
NPM_TOKEN:
@@ -58,3 +65,11 @@ jobs:
GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }}
- name: Notify Discord on failure
if: failure() && inputs.enableFailureNotification == true
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: "GitHub Actions"
embed-title: 'Publish of $${{ inputs.release_channel }} release failed'
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
@@ -21,7 +21,9 @@ jobs:
commit_sha: ${{ github.sha }}
release_channel: stable
dist_tag: canary,next
enableFailureNotification: true
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -40,6 +42,8 @@ jobs:
commit_sha: ${{ github.sha }}
release_channel: experimental
dist_tag: experimental
enableFailureNotification: true
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -2,6 +2,7 @@ name: (Shared) Label Core Team PRs
on:
pull_request_target:
types: [opened]
permissions: {}
@@ -11,7 +12,19 @@ env:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
jobs:
check_access:
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
+1 -1
View File
@@ -1 +1 @@
v18.20.1
v20.19.0
-18
View File
@@ -1,18 +0,0 @@
## March 22, 2024 (18.3.0-canary-670811593-20240322)
## React
- Added `useActionState` to replace `useFormState` and added `pending` value ([#28491](https://github.com/facebook/react/pull/28491)).
## October 5, 2023 (18.3.0-canary-546178f91-20231005)
### React
- Added support for async functions to be passed to `startTransition`.
- `useTransition` now triggers the nearest error boundary instead of a global error.
- Added `useOptimistic`, a new Hook for handling optimistic UI updates. It optimistically updates the UI before receiving confirmation from a server or external source.
### React DOM
- Added support for passing async functions to the `action` prop on `<form>`. When the function passed to `action` is marked with [`'use server'`](https://react.dev/reference/react/use-server), the form is [progressively enhanced](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement).
- Added `useFormStatus`, a new Hook for checking the submission state of a form.
- Added `useFormState`, a new Hook for updating state upon form submission. When the function passed to `useFormState` is marked with [`'use server'`](https://react.dev/reference/react/use-server), the update is [progressively enhanced](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement).
+47
View File
@@ -1,3 +1,50 @@
## 19.1.0 (March 28, 2025)
### Owner Stack
An Owner Stack is a string representing the components that are directly responsible for rendering a particular component. You can log Owner Stacks when debugging or use Owner Stacks to enhance error overlays or other development tools. Owner Stacks are only available in development builds. Component Stacks in production are unchanged.
* An Owner Stack is a development-only stack trace that helps identify which components are responsible for rendering a particular component. An Owner Stack is distinct from a Component Stacks, which shows the hierarchy of components leading to an error.
* The [captureOwnerStack API](https://react.dev/reference/react/captureOwnerStack) is only available in development mode and returns a Owner Stack, if available. The API can be used to enhance error overlays or log component relationships when debugging. [#29923](https://github.com/facebook/react/pull/29923), [#32353](https://github.com/facebook/react/pull/32353), [#30306](https://github.com/facebook/react/pull/30306),
[#32538](https://github.com/facebook/react/pull/32538), [#32529](https://github.com/facebook/react/pull/32529), [#32538](https://github.com/facebook/react/pull/32538)
### React
* Enhanced support for Suspense boundaries to be used anywhere, including the client, server, and during hydration. [#32069](https://github.com/facebook/react/pull/32069), [#32163](https://github.com/facebook/react/pull/32163), [#32224](https://github.com/facebook/react/pull/32224), [#32252](https://github.com/facebook/react/pull/32252)
* Reduced unnecessary client rendering through improved hydration scheduling [#31751](https://github.com/facebook/react/pull/31751)
* Increased priority of client rendered Suspense boundaries [#31776](https://github.com/facebook/react/pull/31776)
* Fixed frozen fallback states by rendering unfinished Suspense boundaries on the client. [#31620](https://github.com/facebook/react/pull/31620)
* Reduced garbage collection pressure by improving Suspense boundary retries. [#31667](https://github.com/facebook/react/pull/31667)
* Fixed erroneous “Waiting for Paint” log when the passive effect phase was not delayed [#31526](https://github.com/facebook/react/pull/31526)
* Fixed a regression causing key warnings for flattened positional children in development mode. [#32117](https://github.com/facebook/react/pull/32117)
* Updated `useId` to use valid CSS selectors, changing format from `:r123:` to `«r123»`. [#32001](https://github.com/facebook/react/pull/32001)
* Added a dev-only warning for null/undefined created in useEffect, useInsertionEffect, and useLayoutEffect. [#32355](https://github.com/facebook/react/pull/32355)
* Fixed a bug where dev-only methods were exported in production builds. React.act is no longer available in production builds. [#32200](https://github.com/facebook/react/pull/32200)
* Improved consistency across prod and dev to improve compatibility with Google Closure Complier and bindings [#31808](https://github.com/facebook/react/pull/31808)
* Improve passive effect scheduling for consistent task yielding. [#31785](https://github.com/facebook/react/pull/31785)
* Fixed asserts in React Native when passChildrenWhenCloningPersistedNodes is enabled for OffscreenComponent rendering. [#32528](https://github.com/facebook/react/pull/32528)
* Fixed component name resolution for Portal [#32640](https://github.com/facebook/react/pull/32640)
* Added support for beforetoggle and toggle events on the dialog element. #32479 [#32479](https://github.com/facebook/react/pull/32479)
### React DOM
* Fixed double warning when the `href` attribute is an empty string [#31783](https://github.com/facebook/react/pull/31783)
* Fixed an edge case where `getHoistableRoot()` didnt work properly when the container was a Document [#32321](https://github.com/facebook/react/pull/32321)
* Removed support for using HTML comments (e.g. `<!-- -->`) as a DOM container. [#32250](https://github.com/facebook/react/pull/32250)
* Added support for `<script>` and `<template>` tags to be nested within `<select>` tags. [#31837](https://github.com/facebook/react/pull/31837)
* Fixed responsive images to be preloaded as HTML instead of headers [#32445](https://github.com/facebook/react/pull/32445)
### use-sync-external-store
* Added `exports` field to `package.json` for `use-sync-external-store` to support various entrypoints. [#25231](https://github.com/facebook/react/pull/25231)
### React Server Components
* Added `unstable_prerender`, a new experimental API for prerendering React Server Components on the server [#31724](https://github.com/facebook/react/pull/31724)
* Fixed an issue where streams would hang when receiving new chunks after a global error [#31840](https://github.com/facebook/react/pull/31840), [#31851](https://github.com/facebook/react/pull/31851)
* Fixed an issue where pending chunks were counted twice. [#31833](https://github.com/facebook/react/pull/31833)
* Added support for streaming in edge environments [#31852](https://github.com/facebook/react/pull/31852)
* Added support for sending custom error names from a server so that they are available in the client for console replaying. [#32116](https://github.com/facebook/react/pull/32116)
* Updated the server component wire format to remove IDs for hints and console.log because they have no return value [#31671](https://github.com/facebook/react/pull/31671)
* Exposed `registerServerReference` in client builds to handle server references in different environments. [#32534](https://github.com/facebook/react/pull/32534)
* Added react-server-dom-parcel package which integrates Server Components with the [Parcel bundler](https://parceljs.org/) [#31725](https://github.com/facebook/react/pull/31725), [#32132](https://github.com/facebook/react/pull/32132), [#31799](https://github.com/facebook/react/pull/31799), [#32294](https://github.com/facebook/react/pull/32294), [#31741](https://github.com/facebook/react/pull/31741)
## 19.0.0 (December 5, 2024)
Below is a list of all new features, APIs, deprecations, and breaking changes. Read [React 19 release post](https://react.dev/blog/2024/04/25/react-19) and [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) for more information.
+7 -7
View File
@@ -18,7 +18,7 @@
//
// 0.0.0-experimental-241c4467e-20200129
const ReactVersion = '19.1.0';
const ReactVersion = '19.2.0';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '5.2.0',
'eslint-plugin-react-hooks': '6.1.0',
'jest-react': '0.17.0',
react: ReactVersion,
'react-art': ReactVersion,
@@ -42,12 +42,12 @@ const stablePackages = {
'react-server-dom-turbopack': ReactVersion,
'react-server-dom-parcel': ReactVersion,
'react-is': ReactVersion,
'react-reconciler': '0.32.0',
'react-refresh': '0.17.0',
'react-reconciler': '0.33.0',
'react-refresh': '0.18.0',
'react-test-renderer': ReactVersion,
'use-subscription': '1.11.0',
'use-sync-external-store': '1.5.0',
scheduler: '0.26.0',
'use-subscription': '1.12.0',
'use-sync-external-store': '1.6.0',
scheduler: '0.27.0',
};
// These packages do not exist in the @canary or @latest channel, only
+1 -1
View File
@@ -27,7 +27,7 @@
"@babel/types": "7.26.3",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.42.1",
"@playwright/test": "^1.51.1",
"@use-gesture/react": "^10.2.22",
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",
+14 -14
View File
@@ -781,12 +781,12 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@playwright/test@^1.42.1":
version "1.47.2"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.47.2.tgz#dbe7051336bfc5cc599954214f9111181dbc7475"
integrity sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==
"@playwright/test@^1.51.1":
version "1.51.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.1.tgz#75357d513221a7be0baad75f01e966baf9c41a2e"
integrity sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==
dependencies:
playwright "1.47.2"
playwright "1.51.1"
"@rtsao/scc@^1.1.0":
version "1.1.0"
@@ -3008,17 +3008,17 @@ pirates@^4.0.1:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
playwright-core@1.47.2:
version "1.47.2"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.47.2.tgz#7858da9377fa32a08be46ba47d7523dbd9460a4e"
integrity sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==
playwright-core@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.1.tgz#d57f0393e02416f32a47cf82b27533656a8acce1"
integrity sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==
playwright@1.47.2:
version "1.47.2"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.47.2.tgz#155688aa06491ee21fb3e7555b748b525f86eb20"
integrity sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==
playwright@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.1.tgz#ae1467ee318083968ad28d6990db59f47a55390f"
integrity sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==
dependencies:
playwright-core "1.47.2"
playwright-core "1.51.1"
optionalDependencies:
fsevents "2.3.2"
+1 -2
View File
@@ -37,7 +37,7 @@
"prettier": "^3.3.3",
"prettier-plugin-hermes-parser": "^0.26.0",
"prompt-promise": "^1.0.3",
"rimraf": "^5.0.10",
"rimraf": "^6.0.1",
"to-fast-properties": "^2.0.0",
"tsup": "^8.4.0",
"typescript": "^5.4.3",
@@ -45,7 +45,6 @@
"yargs": "^17.7.2"
},
"resolutions": {
"rimraf": "5.0.10",
"@babel/types": "7.26.3"
},
"packageManager": "yarn@1.22.22"
@@ -2406,6 +2406,19 @@ function lowerExpression(
kind: 'TypeCastExpression',
value: lowerExpressionToTemporary(builder, expr.get('expression')),
typeAnnotation: typeAnnotation.node,
typeAnnotationKind: 'cast',
type: lowerType(typeAnnotation.node),
loc: exprLoc,
};
}
case 'TSSatisfiesExpression': {
let expr = exprPath as NodePath<t.TSSatisfiesExpression>;
const typeAnnotation = expr.get('typeAnnotation');
return {
kind: 'TypeCastExpression',
value: lowerExpressionToTemporary(builder, expr.get('expression')),
typeAnnotation: typeAnnotation.node,
typeAnnotationKind: 'satisfies',
type: lowerType(typeAnnotation.node),
loc: exprLoc,
};
@@ -2417,6 +2430,7 @@ function lowerExpression(
kind: 'TypeCastExpression',
value: lowerExpressionToTemporary(builder, expr.get('expression')),
typeAnnotation: typeAnnotation.node,
typeAnnotationKind: 'as',
type: lowerType(typeAnnotation.node),
loc: exprLoc,
};
@@ -12,6 +12,7 @@ import {
BasicBlock,
BlockId,
DependencyPathEntry,
FunctionExpression,
GeneratedSource,
getHookKind,
HIRFunction,
@@ -23,6 +24,7 @@ import {
PropertyLiteral,
ReactiveScopeDependency,
ScopeId,
TInstruction,
} from './HIR';
const DEBUG_PRINT = false;
@@ -120,6 +122,33 @@ export function collectHoistablePropertyLoads(
});
}
export function collectHoistablePropertyLoadsInInnerFn(
fnInstr: TInstruction<FunctionExpression>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
): ReadonlyMap<BlockId, BlockInfo> {
const fn = fnInstr.value.loweredFunc.func;
const initialContext: CollectHoistablePropertyLoadsContext = {
temporaries,
knownImmutableIdentifiers: new Set(),
hoistableFromOptionals,
registry: new PropertyPathRegistry(),
nestedFnImmutableContext: null,
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
? new Set()
: getAssumedInvokedFunctions(fn),
};
const nestedFnImmutableContext = new Set(
fn.context
.filter(place =>
isImmutableAtInstr(place.identifier, fnInstr.id, initialContext),
)
.map(place => place.identifier.id),
);
initialContext.nestedFnImmutableContext = nestedFnImmutableContext;
return collectHoistablePropertyLoadsImpl(fn, initialContext);
}
type CollectHoistablePropertyLoadsContext = {
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
@@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInFireFunctionId,
BuiltInFireId,
BuiltInMapId,
BuiltInMixedReadonlyId,
@@ -674,7 +675,12 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
{
positionalParams: [],
restParam: null,
returnType: {kind: 'Primitive'},
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInFireFunctionId,
isConstructor: false,
},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
},
@@ -943,13 +943,21 @@ export type InstructionValue =
value: Place;
loc: SourceLocation;
}
| {
| ({
kind: 'TypeCastExpression';
value: Place;
typeAnnotation: t.FlowType | t.TSType;
type: Type;
loc: SourceLocation;
}
} & (
| {
typeAnnotation: t.FlowType;
typeAnnotationKind: 'cast';
}
| {
typeAnnotation: t.TSType;
typeAnnotationKind: 'as' | 'satisfies';
}
))
| JsxExpression
| {
kind: 'ObjectExpression';
@@ -1747,6 +1755,12 @@ export function isDispatcherType(id: Identifier): boolean {
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInDispatch';
}
export function isFireFunctionType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInFireFunction'
);
}
export function isStableType(id: Identifier): boolean {
return (
isSetStateType(id) ||
@@ -223,6 +223,7 @@ export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
// ShapeRegistry with default definitions for built-ins.
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
@@ -110,7 +110,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
}
}
function findTemporariesUsedOutsideDeclaringScope(
export function findTemporariesUsedOutsideDeclaringScope(
fn: HIRFunction,
): ReadonlySet<DeclarationId> {
/*
@@ -372,7 +372,7 @@ type Decl = {
scope: Stack<ReactiveScope>;
};
class Context {
export class DependencyCollectionContext {
#declarations: Map<DeclarationId, Decl> = new Map();
#reassignments: Map<Identifier, Decl> = new Map();
@@ -642,7 +642,10 @@ enum HIRValue {
Terminal,
}
function handleInstruction(instr: Instruction, context: Context): void {
export function handleInstruction(
instr: Instruction,
context: DependencyCollectionContext,
): void {
const {id, value, lvalue} = instr;
context.declare(lvalue.identifier, {
id,
@@ -725,7 +728,7 @@ function collectDependencies(
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
const context = new Context(
const context = new DependencyCollectionContext(
usedOutsideDeclaringScope,
temporaries,
processedInstrsInOptional,
@@ -14,17 +14,30 @@ import {
ScopeId,
ReactiveScopeDependency,
Place,
ReactiveScope,
ReactiveScopeDependencies,
Terminal,
isUseRefType,
isSetStateType,
isFireFunctionType,
makeScopeId,
} from '../HIR';
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
import {ReactiveScopeDependencyTreeHIR} from '../HIR/DeriveMinimalDependenciesHIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {
createTemporaryPlace,
fixScopeAndIdentifierRanges,
markInstructionIds,
} from '../HIR/HIRBuilder';
import {
collectTemporariesSidemap,
DependencyCollectionContext,
handleInstruction,
} from '../HIR/PropagateScopeDependenciesHIR';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {empty} from '../Utils/Stack';
import {getOrInsertWith} from '../Utils/utils';
/**
@@ -53,10 +66,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
const autodepFnLoads = new Map<IdentifierId, number>();
const autodepModuleLoads = new Map<IdentifierId, Map<string, number>>();
const scopeInfos = new Map<
ScopeId,
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
>();
const scopeInfos = new Map<ScopeId, ReactiveScopeDependencies>();
const loadGlobals = new Set<IdentifierId>();
@@ -70,19 +80,18 @@ export function inferEffectDependencies(fn: HIRFunction): void {
const reactiveIds = inferReactiveIdentifiers(fn);
for (const [, block] of fn.body.blocks) {
if (
block.terminal.kind === 'scope' ||
block.terminal.kind === 'pruned-scope'
) {
if (block.terminal.kind === 'scope') {
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
scopeInfos.set(block.terminal.scope.id, {
pruned: block.terminal.kind === 'pruned-scope',
deps: block.terminal.scope.dependencies,
hasSingleInstr:
scopeBlock.instructions.length === 1 &&
scopeBlock.terminal.kind === 'goto' &&
scopeBlock.terminal.block === block.terminal.fallthrough,
});
if (
scopeBlock.instructions.length === 1 &&
scopeBlock.terminal.kind === 'goto' &&
scopeBlock.terminal.block === block.terminal.fallthrough
) {
scopeInfos.set(
block.terminal.scope.id,
block.terminal.scope.dependencies,
);
}
}
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
for (const instr of block.instructions) {
@@ -164,22 +173,12 @@ export function inferEffectDependencies(fn: HIRFunction): void {
fnExpr.lvalue.identifier.scope != null
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
: null;
CompilerError.invariant(scopeInfo != null, {
reason: 'Expected function expression scope to exist',
loc: value.loc,
});
if (scopeInfo.pruned || !scopeInfo.hasSingleInstr) {
/**
* TODO: retry pipeline that ensures effect function expressions
* are placed into their own scope
*/
CompilerError.throwTodo({
reason:
'[InferEffectDependencies] Expected effect function to have non-pruned scope and its scope to have exactly one instruction',
loc: fnExpr.loc,
});
let minimalDeps: Set<ReactiveScopeDependency>;
if (scopeInfo != null) {
minimalDeps = new Set(scopeInfo);
} else {
minimalDeps = inferMinimalDependencies(fnExpr);
}
/**
* Step 1: push dependencies to the effect deps array
*
@@ -187,11 +186,13 @@ export function inferEffectDependencies(fn: HIRFunction): void {
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
* explanation.
*/
for (const dep of scopeInfo.deps) {
for (const dep of minimalDeps) {
if (
(isUseRefType(dep.identifier) ||
((isUseRefType(dep.identifier) ||
isSetStateType(dep.identifier)) &&
!reactiveIds.has(dep.identifier.id)
!reactiveIds.has(dep.identifier.id)) ||
isFireFunctionType(dep.identifier)
) {
// exclude non-reactive hook results, which will never be in a memo block
continue;
@@ -338,3 +339,132 @@ function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
}
return reactiveIds;
}
function inferMinimalDependencies(
fnInstr: TInstruction<FunctionExpression>,
): Set<ReactiveScopeDependency> {
const fn = fnInstr.value.loweredFunc.func;
const temporaries = collectTemporariesSidemap(fn, new Set());
const {
hoistableObjects,
processedInstrsInOptional,
temporariesReadInOptional,
} = collectOptionalChainSidemap(fn);
const hoistablePropertyLoads = collectHoistablePropertyLoadsInInnerFn(
fnInstr,
temporaries,
hoistableObjects,
);
const hoistableToFnEntry = hoistablePropertyLoads.get(fn.body.entry);
CompilerError.invariant(hoistableToFnEntry != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing entry block',
loc: fnInstr.loc,
});
const dependencies = inferDependencies(
fnInstr,
new Map([...temporaries, ...temporariesReadInOptional]),
processedInstrsInOptional,
);
const tree = new ReactiveScopeDependencyTreeHIR(
[...hoistableToFnEntry.assumedNonNullObjects].map(o => o.fullPath),
);
for (const dep of dependencies) {
tree.addDependency({...dep});
}
return tree.deriveMinimalDependencies();
}
function inferDependencies(
fnInstr: TInstruction<FunctionExpression>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Set<ReactiveScopeDependency> {
const fn = fnInstr.value.loweredFunc.func;
const context = new DependencyCollectionContext(
new Set(),
temporaries,
processedInstrsInOptional,
);
for (const dep of fn.context) {
context.declare(dep.identifier, {
id: makeInstructionId(0),
scope: empty(),
});
}
const placeholderScope: ReactiveScope = {
id: makeScopeId(0),
range: {
start: fnInstr.id,
end: makeInstructionId(fnInstr.id + 1),
},
dependencies: new Set(),
reassignments: new Set(),
declarations: new Map(),
earlyReturnValue: null,
merged: new Set(),
loc: GeneratedSource,
};
context.enterScope(placeholderScope);
inferDependenciesInFn(fn, context, temporaries);
context.exitScope(placeholderScope, false);
const resultUnfiltered = context.deps.get(placeholderScope);
CompilerError.invariant(resultUnfiltered != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
loc: fn.loc,
});
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
const result = new Set<ReactiveScopeDependency>();
for (const dep of resultUnfiltered) {
if (fnContext.has(dep.identifier.id)) {
result.add(dep);
}
}
return result;
}
function inferDependenciesInFn(
fn: HIRFunction,
context: DependencyCollectionContext,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
): void {
for (const [, block] of fn.body.blocks) {
// Record referenced optional chains in phis
for (const phi of block.phis) {
for (const operand of phi.operands) {
const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
if (maybeOptionalChain) {
context.visitDependency(maybeOptionalChain);
}
}
}
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
context.declare(instr.lvalue.identifier, {
id: instr.id,
scope: context.currentScope,
});
/**
* Recursively visit the inner function to extract dependencies
*/
const innerFn = instr.value.loweredFunc.func;
context.enterInnerFn(instr as TInstruction<FunctionExpression>, () => {
inferDependenciesInFn(innerFn, context, temporaries);
});
} else {
handleInstruction(instr, context);
}
}
}
}
@@ -2131,10 +2131,17 @@ function codegenInstructionValue(
}
case 'TypeCastExpression': {
if (t.isTSType(instrValue.typeAnnotation)) {
value = t.tsAsExpression(
codegenPlaceToExpression(cx, instrValue.value),
instrValue.typeAnnotation,
);
if (instrValue.typeAnnotationKind === 'satisfies') {
value = t.tsSatisfiesExpression(
codegenPlaceToExpression(cx, instrValue.value),
instrValue.typeAnnotation,
);
} else {
value = t.tsAsExpression(
codegenPlaceToExpression(cx, instrValue.value),
instrValue.typeAnnotation,
);
}
} else {
value = t.typeCastExpression(
codegenPlaceToExpression(cx, instrValue.value),
@@ -34,7 +34,11 @@ import {
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {getOrInsertWith} from '../Utils/utils';
import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape';
import {
BuiltInFireFunctionId,
BuiltInFireId,
DefaultNonmutatingHook,
} from '../HIR/ObjectShape';
import {eachInstructionOperand} from '../HIR/visitors';
import {printSourceLocationLine} from '../HIR/PrintHIR';
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
@@ -633,6 +637,13 @@ class Context {
() => createTemporaryPlace(this.#env, GeneratedSource),
);
fireFunctionBinding.identifier.type = {
kind: 'Function',
shapeId: BuiltInFireFunctionId,
return: {kind: 'Poly'},
isConstructor: false,
};
this.#capturedCalleeIdentifierIds.set(callee.identifier.id, {
fireFunctionBinding,
capturedCalleeIdentifier: callee.identifier,
@@ -0,0 +1,39 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function Component({foo}) {
const arr = [];
// Taking either arr[0].value or arr as a dependency is reasonable
// as long as developers know what to expect.
useEffect(() => print(arr[0].value));
arr.push({value: foo});
return arr;
}
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
import { useEffect } from "react";
import { print } from "shared-runtime";
function Component(t0) {
const { foo } = t0;
const arr = [];
useEffect(() => print(arr[0].value), [arr[0].value]);
arr.push({ value: foo });
return arr;
}
```
### Eval output
(kind: exception) Fixture not implemented
@@ -0,0 +1,12 @@
// @inferEffectDependencies @panicThreshold(none)
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function Component({foo}) {
const arr = [];
// Taking either arr[0].value or arr as a dependency is reasonable
// as long as developers know what to expect.
useEffect(() => print(arr[0].value));
arr.push({value: foo});
return arr;
}
@@ -0,0 +1,38 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useEffect, useRef} from 'react';
import {print} from 'shared-runtime';
function Component({arrRef}) {
// Avoid taking arr.current as a dependency
useEffect(() => print(arrRef.current));
arr.current.val = 2;
return arr;
}
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
import { useEffect, useRef } from "react";
import { print } from "shared-runtime";
function Component(t0) {
const { arrRef } = t0;
useEffect(() => print(arrRef.current), [arrRef]);
arr.current.val = 2;
return arr;
}
```
### Eval output
(kind: exception) Fixture not implemented
@@ -0,0 +1,11 @@
// @inferEffectDependencies @panicThreshold(none)
import {useEffect, useRef} from 'react';
import {print} from 'shared-runtime';
function Component({arrRef}) {
// Avoid taking arr.current as a dependency
useEffect(() => print(arrRef.current));
arr.current.val = 2;
return arr;
}
@@ -0,0 +1,34 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useEffect} from 'react';
function Component({foo}) {
const arr = [];
useEffect(() => arr.push(foo));
arr.push(2);
return arr;
}
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
import { useEffect } from "react";
function Component(t0) {
const { foo } = t0;
const arr = [];
useEffect(() => arr.push(foo), [arr, foo]);
arr.push(2);
return arr;
}
```
### Eval output
(kind: exception) Fixture not implemented
@@ -0,0 +1,9 @@
// @inferEffectDependencies @panicThreshold(none)
import {useEffect} from 'react';
function Component({foo}) {
const arr = [];
useEffect(() => arr.push(foo));
arr.push(2);
return arr;
}
@@ -1,42 +0,0 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useRef} from 'react';
import {useSpecialEffect} from 'shared-runtime';
/**
* The retry pipeline disables memoization features, which means we need to
* provide an alternate implementation of effect dependencies which does not
* rely on memoization.
*/
function useFoo({cond}) {
const ref = useRef();
const derived = cond ? ref.current : makeObject();
useSpecialEffect(() => {
log(derived);
}, [derived]);
return ref;
}
```
## Error
```
11 | const ref = useRef();
12 | const derived = cond ? ref.current : makeObject();
> 13 | useSpecialEffect(() => {
| ^^^^^^^^^^^^^^^^^^^^^^^^
> 14 | log(derived);
| ^^^^^^^^^^^^^^^^^
> 15 | }, [derived]);
| ^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.. (Bailout reason: Invariant: Expected function expression scope to exist (13:15)) (13:15)
16 | return ref;
17 | }
18 |
```
@@ -0,0 +1,54 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useRef} from 'react';
import {useSpecialEffect} from 'shared-runtime';
/**
* The retry pipeline disables memoization features, which means we need to
* provide an alternate implementation of effect dependencies which does not
* rely on memoization.
*/
function useFoo({cond}) {
const ref = useRef();
const derived = cond ? ref.current : makeObject();
useSpecialEffect(() => {
log(derived);
}, [derived]);
return ref;
}
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
import { useRef } from "react";
import { useSpecialEffect } from "shared-runtime";
/**
* The retry pipeline disables memoization features, which means we need to
* provide an alternate implementation of effect dependencies which does not
* rely on memoization.
*/
function useFoo(t0) {
const { cond } = t0;
const ref = useRef();
const derived = cond ? ref.current : makeObject();
useSpecialEffect(
() => {
log(derived);
},
[derived],
[derived],
);
return ref;
}
```
### Eval output
(kind: exception) Fixture not implemented
@@ -49,7 +49,7 @@ function Component(props) {
} else {
t2 = $[4];
}
useEffect(t2, [t1, props]);
useEffect(t2, [props]);
return null;
}
@@ -0,0 +1,71 @@
## Input
```javascript
// @enableUseTypeAnnotations
function Component(props: {id: number}) {
const x = makeArray(props.id) satisfies number[];
const y = x.at(0);
return y;
}
function makeArray<T>(x: T): Array<T> {
return [x];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{id: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableUseTypeAnnotations
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] !== props.id) {
t0 = makeArray(props.id);
$[0] = props.id;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0 satisfies number[];
let t1;
if ($[2] !== x) {
t1 = x.at(0);
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
const y = t1;
return y;
}
function makeArray(x) {
const $ = _c(2);
let t0;
if ($[0] !== x) {
t0 = [x];
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ id: 42 }],
};
```
### Eval output
(kind: ok) 42
@@ -0,0 +1,15 @@
// @enableUseTypeAnnotations
function Component(props: {id: number}) {
const x = makeArray(props.id) satisfies number[];
const y = x.at(0);
return y;
}
function makeArray<T>(x: T): Array<T> {
return [x];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{id: 42}],
};
@@ -0,0 +1,41 @@
## Input
```javascript
// @enableUseTypeAnnotations
import {identity} from 'shared-runtime';
function Component(props: {id: number}) {
const x = identity(props.id);
const y = x satisfies number;
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{id: 42}],
};
```
## Code
```javascript
// @enableUseTypeAnnotations
import { identity } from "shared-runtime";
function Component(props) {
const x = identity(props.id);
const y = x satisfies number;
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ id: 42 }],
};
```
### Eval output
(kind: ok) 42
@@ -0,0 +1,13 @@
// @enableUseTypeAnnotations
import {identity} from 'shared-runtime';
function Component(props: {id: number}) {
const x = identity(props.id);
const y = x satisfies number;
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{id: 42}],
};
@@ -92,36 +92,8 @@ const tests: CompilerTestCases = {
}
`,
},
{
// Don't report the issue if Flow already has
name: '[InvalidInput] Ref access during render',
code: normalizeIndent`
function Component(props) {
const ref = useRef(null);
// $FlowFixMe[react-rule-unsafe-ref]
const value = ref.current;
return value;
}
`,
},
],
invalid: [
{
name: '[InvalidInput] Ref access during render',
code: normalizeIndent`
function Component(props) {
const ref = useRef(null);
const value = ref.current;
return value;
}
`,
errors: [
{
message:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
},
],
},
{
name: 'Reportable levels can be configured',
options: [{reportableLevels: new Set([ErrorSeverity.Todo])}],
@@ -105,6 +105,9 @@ const COMPILER_OPTIONS: Partial<PluginOptions> = {
panicThreshold: 'none',
// Don't emit errors on Flow suppressions--Flow already gave a signal
flowSuppressions: false,
environment: validateEnvironmentConfig({
validateRefAccessDuringRender: false,
}),
};
const rule: Rule.RuleModule = {
@@ -149,10 +152,14 @@ const rule: Rule.RuleModule = {
}
let shouldReportUnusedOptOutDirective = true;
const options: PluginOptions = {
...parsePluginOptions(userOpts),
const options: PluginOptions = parsePluginOptions({
...COMPILER_OPTIONS,
};
...userOpts,
environment: {
...COMPILER_OPTIONS.environment,
...userOpts.environment,
},
});
const userLogger: Logger | null = options.logger;
options.logger = {
logEvent: (filename, event): void => {
@@ -0,0 +1,22 @@
# React MCP Server (experimental)
An experimental MCP Server for React.
## Development
First, add this file if you're using Claude Desktop: `code ~/Library/Application\ Support/Claude/claude_desktop_config.json`. Copy the absolute path from `which node` and from `react/compiler/react-mcp-server/dist/index.js` and paste, for example:
```json
{
"mcpServers": {
"react": {
"command": "/Users/<username>/.asdf/shims/node",
"args": [
"/Users/<username>/code/react/compiler/packages/react-mcp-server/dist/index.js"
]
}
}
}
```
Next, run `yarn workspace react-mcp-server watch` from the `react/compiler` directory and make changes as needed. You will need to restart Claude everytime you want to try your changes.
@@ -0,0 +1,35 @@
{
"name": "react-mcp-server",
"version": "0.0.0",
"description": "React MCP Server (experimental)",
"bin": {
"react-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "rimraf dist && tsup",
"test": "echo 'no tests'",
"dev": "concurrently --kill-others -n build,inspect \"yarn run watch\" \"wait-on dist/index.js && yarn run inspect\"",
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
"watch": "yarn build --watch"
},
"dependencies": {
"@babel/core": "^7.26.0",
"@babel/parser": "^7.26",
"@babel/plugin-syntax-typescript": "^7.25.9",
"@modelcontextprotocol/sdk": "^1.9.0",
"algoliasearch": "^5.23.3",
"cheerio": "^1.0.0",
"prettier": "^3.3.3",
"turndown": "^7.2.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/turndown": "^5.0.5"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react.git",
"directory": "compiler/packages/react-mcp-server"
}
}
@@ -0,0 +1,77 @@
/**
* 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 prettier from 'prettier';
export let lastResult: BabelCore.BabelFileResult | null = null;
export type PrintedCompilerPipelineValue =
| {
kind: 'hir';
name: string;
fnName: string | null;
value: string;
}
| {kind: 'reactive'; name: string; fnName: string | null; value: string}
| {kind: 'debug'; name: string; fnName: string | null; value: string};
type CompileOptions = {
text: string;
file: string;
options: Partial<PluginOptions> | null;
};
export async function compile({
text,
file,
options,
}: CompileOptions): Promise<BabelCore.BabelFileResult> {
const ast = await parseAsync(text, {
sourceFileName: file,
parserOpts: {
plugins: ['typescript', 'jsx'],
},
sourceType: 'module',
});
if (ast == null) {
throw new Error('Could not parse');
}
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}`,
);
}
try {
result.code = await prettier.format(result.code, {
semi: false,
parser: 'babel-ts',
});
if (result.code != null) {
lastResult = result;
}
} catch (err) {
// If prettier failed just log, no need to crash
console.error(err);
}
return result;
}
@@ -0,0 +1,397 @@
/**
* 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 {
McpServer,
ResourceTemplate,
} from '@modelcontextprotocol/sdk/server/mcp.js';
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
import {z} from 'zod';
import {compile, type PrintedCompilerPipelineValue} from './compiler';
import {
CompilerPipelineValue,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
PluginOptions,
SourceLocation,
} from 'babel-plugin-react-compiler/src';
import * as cheerio from 'cheerio';
import TurndownService from 'turndown';
import {queryAlgolia} from './utils/algolia';
import assertExhaustive from './utils/assertExhaustive';
const turndownService = new TurndownService();
const server = new McpServer({
name: 'React',
version: '0.0.0',
});
function slugify(heading: string): string {
return heading
.split(' ')
.map(w => w.toLowerCase())
.join('-');
}
// TODO: how to verify this works?
server.resource(
'docs',
new ResourceTemplate('docs://{message}', {list: undefined}),
async (_uri, {message}) => {
const hits = await queryAlgolia(message);
const deduped = new Map();
for (const hit of hits) {
// drop hashes to dedupe properly
const u = new URL(hit.url);
if (deduped.has(u.pathname)) {
continue;
}
deduped.set(u.pathname, hit);
}
const pages: Array<string | null> = await Promise.all(
Array.from(deduped.values()).map(hit => {
return fetch(hit.url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
},
}).then(res => {
if (res.ok === true) {
return res.text();
} else {
console.error(
`Could not fetch docs: ${res.status} ${res.statusText}`,
);
return null;
}
});
}),
);
const resultsMarkdown = pages
.filter(html => html !== null)
.map(html => {
const $ = cheerio.load(html);
const title = encodeURIComponent(slugify($('h1').text()));
// react.dev should always have at least one <article> with the main content
const article = $('article').html();
if (article != null) {
return {
uri: `docs://${title}`,
text: turndownService.turndown(article),
};
} else {
return {
uri: `docs://${title}`,
// Fallback to converting the whole page to markdown
text: turndownService.turndown($.html()),
};
}
});
return {
contents: resultsMarkdown,
};
},
);
server.tool(
'compile',
'Compile code with React Compiler. Optionally, for debugging provide a pass name like "HIR" to see more information.',
{
text: z.string(),
passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(),
},
async ({text, passName}) => {
const pipelinePasses = new Map<
string,
Array<PrintedCompilerPipelineValue>
>();
const recordPass: (
result: PrintedCompilerPipelineValue,
) => void = result => {
const entry = pipelinePasses.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
pipelinePasses.set(result.name, [result]);
}
};
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
recordPass({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
recordPass({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
recordPass({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
assertExhaustive(result, `Unhandled result ${result}`);
}
}
};
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
const compilerOptions: Partial<PluginOptions> = {
panicThreshold: 'none',
logger: {
debugLogIRs: logIR,
logEvent: (_filename, event): void => {
if (event.kind === 'CompileError') {
const detail = event.detail;
const loc =
detail.loc == null || typeof detail.loc == 'symbol'
? event.fnLoc
: detail.loc;
errors.push({
message: detail.reason,
loc,
});
}
},
},
};
try {
const result = await compile({
text,
file: 'anonymous.tsx',
options: compilerOptions,
});
if (result.code == null) {
return {
isError: true,
content: [{type: 'text' as const, text: 'Error: Could not compile'}],
};
}
const requestedPasses: Array<{type: 'text'; text: string}> = [];
if (passName != null) {
switch (passName) {
case 'All': {
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
if (hir !== undefined) {
for (const pipelineValue of hir) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
}
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
if (reactiveFunc !== undefined) {
for (const pipelineValue of reactiveFunc) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
}
break;
}
case 'HIR': {
// Last pass before HIR -> ReactiveFunction
const requestedPass = pipelinePasses.get(
'PropagateScopeDependenciesHIR',
);
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
} else {
console.error(`Could not find requested pass ${passName}`);
}
break;
}
case 'ReactiveFunction': {
// Last pass
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
} else {
console.error(`Could not find requested pass ${passName}`);
}
break;
}
case '@DEBUG': {
for (const [, pipelinePass] of pipelinePasses) {
for (const pass of pipelinePass) {
requestedPasses.push({
type: 'text' as const,
text: `${pass.name}\n\n${pass.value}`,
});
}
}
break;
}
default: {
assertExhaustive(
passName,
`Unhandled passName option: ${passName}`,
);
}
}
const requestedPass = pipelinePasses.get(passName);
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
if (pipelineValue.name === passName) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
}
}
}
if (errors.length > 0) {
return {
content: errors.map(err => {
return {
type: 'text' as const,
text:
err.loc === null || typeof err.loc === 'symbol'
? `React Compiler bailed out:\n\n${err.message}`
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`,
};
}),
};
}
return {
content: [
{type: 'text' as const, text: result.code},
...requestedPasses,
],
};
} catch (err) {
return {
isError: true,
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
};
}
},
);
server.prompt('review-react-code', () => ({
messages: [
{
role: 'assistant',
content: {
type: 'text',
text: `
## Role
You are a React assistant that helps users write more efficient and optimizable React code. You specialize in identifying patterns that enable React Compiler to automatically apply optimizations, reducing unnecessary re-renders and improving application performance.
## Follow these guidelines in all code you produce and suggest
Use functional components with Hooks: Do not generate class components or use old lifecycle methods. Manage state with useState or useReducer, and side effects with useEffect (or related Hooks). Always prefer functions and Hooks for any new component logic.
Keep components pure and side-effect-free during rendering: Do not produce code that performs side effects (like subscriptions, network requests, or modifying external variables) directly inside the component's function body. Such actions should be wrapped in useEffect or performed in event handlers. Ensure your render logic is a pure function of props and state.
Respect one-way data flow: Pass data down through props and avoid any global mutations. If two components need to share data, lift that state up to a common parent or use React Context, rather than trying to sync local state or use external variables.
Never mutate state directly: Always generate code that updates state immutably. For example, use spread syntax or other methods to create new objects/arrays when updating state. Do not use assignments like state.someValue = ... or array mutations like array.push() on state variables. Use the state setter (setState from useState, etc.) to update state.
Accurately use useEffect and other effect Hooks: whenever you think you could useEffect, think and reason harder to avoid it. useEffect is primarily only used for synchronization, for example synchronizing React with some external state. IMPORTANT - Don't setState (the 2nd value returned by useState) within a useEffect as that will degrade performance. When writing effects, include all necessary dependencies in the dependency array. Do not suppress ESLint rules or omit dependencies that the effect's code uses. Structure the effect callbacks to handle changing values properly (e.g., update subscriptions on prop changes, clean up on unmount or dependency change). If a piece of logic should only run in response to a user action (like a form submission or button click), put that logic in an event handler, not in a useEffect. Where possible, useEffects should return a cleanup function.
Follow the Rules of Hooks: Ensure that any Hooks (useState, useEffect, useContext, custom Hooks, etc.) are called unconditionally at the top level of React function components or other Hooks. Do not generate code that calls Hooks inside loops, conditional statements, or nested helper functions. Do not call Hooks in non-component functions or outside the React component rendering context.
Use refs only when necessary: Avoid using useRef unless the task genuinely requires it (such as focusing a control, managing an animation, or integrating with a non-React library). Do not use refs to store application state that should be reactive. If you do use refs, never write to or read from ref.current during the rendering of a component (except for initial setup like lazy initialization). Any ref usage should not affect the rendered output directly.
Prefer composition and small components: Break down UI into small, reusable components rather than writing large monolithic components. The code you generate should promote clarity and reusability by composing components together. Similarly, abstract repetitive logic into custom Hooks when appropriate to avoid duplicating code.
Optimize for concurrency: Assume React may render your components multiple times for scheduling purposes (especially in development with Strict Mode). Write code that remains correct even if the component function runs more than once. For instance, avoid side effects in the component body and use functional state updates (e.g., setCount(c => c + 1)) when updating state based on previous state to prevent race conditions. Always include cleanup functions in effects that subscribe to external resources. Don't write useEffects for "do this when this changes" side-effects. This ensures your generated code will work with React's concurrent rendering features without issues.
Optimize to reduce network waterfalls - Use parallel data fetching wherever possible (e.g., start multiple requests at once rather than one after another). Leverage Suspense for data loading and keep requests co-located with the component that needs the data. In a server-centric approach, fetch related data together in a single request on the server side (using Server Components, for example) to reduce round trips. Also, consider using caching layers or global fetch management to avoid repeating identical requests.
Rely on React Compiler - useMemo, useCallback, and React.memo can be omitted if React Compiler is enabled. Avoid premature optimization with manual memoization. Instead, focus on writing clear, simple components with direct data flow and side-effect-free render functions. Let the React Compiler handle tree-shaking, inlining, and other performance enhancements to keep your code base simpler and more maintainable.
Design for a good user experience - Provide clear, minimal, and non-blocking UI states. When data is loading, show lightweight placeholders (e.g., skeleton screens) rather than intrusive spinners everywhere. Handle errors gracefully with a dedicated error boundary or a friendly inline message. Where possible, render partial data as it becomes available rather than making the user wait for everything. Suspense allows you to declare the loading states in your component tree in a natural way, preventing flash states and improving perceived performance.
Server Components - Shift data-heavy logic to the server whenever possible. Break up the more static parts of the app into server components. Break up data fetching into server components. Only client components (denoted by the 'use client' top level directive) need interactivity. By rendering parts of your UI on the server, you reduce the client-side JavaScript needed and avoid sending unnecessary data over the wire. Use Server Components to prefetch and pre-render data, allowing faster initial loads and smaller bundle sizes. This also helps manage or eliminate certain waterfalls by resolving data on the server before streaming the HTML (and partial React tree) to the client.
## Available Resources
- 'docs': Look up documentation from docs://{query}. Returns markdown as a string.
## Available Tools
- 'compile': Run the user's code through React Compiler. Returns optimized JS/TS code with potential diagnostics.
## Process
1. Analyze the user's code for optimization opportunities:
- Check for React anti-patterns that prevent compiler optimization
- Identify unnecessary manual optimizations (useMemo, useCallback, React.memo) that the compiler can handle
- Look for component structure issues that limit compiler effectiveness
- Think about each suggestion you are making and consult React docs using the docs://{query} resource for best practices
2. Use React Compiler to verify optimization potential:
- Run the code through the compiler and analyze the output
- You can run the compiler multiple times to verify your work
- Check for successful optimization by looking for const $ = _c(n) cache entries, where n is an integer
- Identify bailout messages that indicate where code could be improved
- Compare before/after optimization potential
3. Provide actionable guidance:
- Explain specific code changes with clear reasoning
- Show before/after examples when suggesting changes
- Include compiler results to demonstrate the impact of optimizations
- Only suggest changes that meaningfully improve optimization potential
## Optimization Guidelines
- Avoid mutation of values that are memoized by the compiler
- State updates should be structured to enable granular updates
- Side effects should be isolated and dependencies clearly defined
- The compiler automatically inserts memoization, so manually added useMemo/useCallback/React.memo can often be removed
## Understanding Compiler Output
- Successful optimization adds import { c as _c } from "react/compiler-runtime";
- Successful optimization initializes a constant sized cache with const $ = _c(n), where n is the size of the cache as an integer
- When suggesting changes, try to increase or decrease the number of cached expressions (visible in const $ = _c(n))
- Increase: more memoization coverage
- Decrease: if there are unnecessary dependencies, less dependencies mean less re-rendering
`,
},
},
],
}));
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('React Compiler MCP Server running on stdio');
}
main().catch(error => {
console.error('Fatal error in main():', error);
process.exit(1);
});
@@ -0,0 +1,93 @@
// https://github.com/algolia/docsearch/blob/15ebcba606b281aa0dddc4ccb8feb19d396bf79e/packages/docsearch-react/src/types/DocSearchHit.ts
type ContentType =
| 'content'
| 'lvl0'
| 'lvl1'
| 'lvl2'
| 'lvl3'
| 'lvl4'
| 'lvl5'
| 'lvl6';
interface DocSearchHitAttributeHighlightResult {
value: string;
matchLevel: 'full' | 'none' | 'partial';
matchedWords: string[];
fullyHighlighted?: boolean;
}
interface DocSearchHitHighlightResultHierarchy {
lvl0: DocSearchHitAttributeHighlightResult;
lvl1: DocSearchHitAttributeHighlightResult;
lvl2: DocSearchHitAttributeHighlightResult;
lvl3: DocSearchHitAttributeHighlightResult;
lvl4: DocSearchHitAttributeHighlightResult;
lvl5: DocSearchHitAttributeHighlightResult;
lvl6: DocSearchHitAttributeHighlightResult;
}
interface DocSearchHitHighlightResult {
content: DocSearchHitAttributeHighlightResult;
hierarchy: DocSearchHitHighlightResultHierarchy;
hierarchy_camel: DocSearchHitHighlightResultHierarchy[];
}
interface DocSearchHitAttributeSnippetResult {
value: string;
matchLevel: 'full' | 'none' | 'partial';
}
interface DocSearchHitSnippetResult {
content: DocSearchHitAttributeSnippetResult;
hierarchy: DocSearchHitHighlightResultHierarchy;
hierarchy_camel: DocSearchHitHighlightResultHierarchy[];
}
export declare type DocSearchHit = {
objectID: string;
content: string | null;
url: string;
url_without_anchor: string;
type: ContentType;
anchor: string | null;
hierarchy: {
lvl0: string;
lvl1: string;
lvl2: string | null;
lvl3: string | null;
lvl4: string | null;
lvl5: string | null;
lvl6: string | null;
};
_highlightResult: DocSearchHitHighlightResult;
_snippetResult: DocSearchHitSnippetResult;
_rankingInfo?: {
promoted: boolean;
nbTypos: number;
firstMatchedWord: number;
proximityDistance?: number;
geoDistance: number;
geoPrecision?: number;
nbExactWords: number;
words: number;
filters: number;
userScore: number;
matchedGeoLocation?: {
lat: number;
lng: number;
distance: number;
};
};
_distinctSeqID?: number;
__autocomplete_indexName?: string;
__autocomplete_queryID?: string;
__autocomplete_algoliaCredentials?: {
appId: string;
apiKey: string;
};
__autocomplete_id?: number;
};
export type InternalDocSearchHit = DocSearchHit & {
__docsearch_parent: InternalDocSearchHit | null;
};
@@ -0,0 +1,91 @@
/**
* 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 {DocSearchHit, InternalDocSearchHit} from '../types/algolia';
import {liteClient, type Hit, type SearchResponse} from 'algoliasearch/lite';
// https://github.com/reactjs/react.dev/blob/55986965fbf69c2584040039c9586a01bd54eba7/src/siteConfig.js#L15-L19
const ALGOLIA_CONFIG = {
appId: '1FCF9AYYAT',
apiKey: '1b7ad4e1c89e645e351e59d40544eda1',
indexName: 'beta-react',
};
export const ALGOLIA_CLIENT = liteClient(
ALGOLIA_CONFIG.appId,
ALGOLIA_CONFIG.apiKey,
);
export function printHierarchy(
hit: DocSearchHit | InternalDocSearchHit,
): string {
let val = `${hit.hierarchy.lvl0} > ${hit.hierarchy.lvl1}`;
if (hit.hierarchy.lvl2 != null) {
val = val.concat(` > ${hit.hierarchy.lvl2}`);
}
if (hit.hierarchy.lvl3 != null) {
val = val.concat(` > ${hit.hierarchy.lvl3}`);
}
if (hit.hierarchy.lvl4 != null) {
val = val.concat(` > ${hit.hierarchy.lvl4}`);
}
if (hit.hierarchy.lvl5 != null) {
val = val.concat(` > ${hit.hierarchy.lvl5}`);
}
if (hit.hierarchy.lvl6 != null) {
val = val.concat(` > ${hit.hierarchy.lvl6}`);
}
return val;
}
export async function queryAlgolia(
message: string | Array<string>,
): Promise<Hit<DocSearchHit>[]> {
const {results} = await ALGOLIA_CLIENT.search<DocSearchHit>({
requests: [
{
query: Array.isArray(message) ? message.join('\n') : message,
indexName: ALGOLIA_CONFIG.indexName,
attributesToRetrieve: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
'url',
],
attributesToSnippet: [
`hierarchy.lvl1:10`,
`hierarchy.lvl2:10`,
`hierarchy.lvl3:10`,
`hierarchy.lvl4:10`,
`hierarchy.lvl5:10`,
`hierarchy.lvl6:10`,
`content:10`,
],
snippetEllipsisText: '…',
hitsPerPage: 30,
attributesToHighlight: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
],
},
],
});
const firstResult = results[0] as SearchResponse<DocSearchHit>;
const {hits} = firstResult;
return hits;
}
@@ -0,0 +1,13 @@
/**
* 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.
*/
/**
* Trigger an exhaustiveness check in TypeScript and throw at runtime.
*/
export default function assertExhaustive(_: never, errorMsg: string): never {
throw new Error(errorMsg);
}
@@ -0,0 +1,5 @@
TODO
- [ ] If code doesnt compile, read diagnostics and try again
- [ ] Provide detailed examples in assistant prompt (use another LLM to generate good prompts, iterate from there)
- [ ] Provide more tools for working with HIR/AST (eg so we can prompt it to try and optimize code via HIR, which it can then translate back into user code changes)
@@ -0,0 +1,22 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"rootDir": "../",
"noEmit": true,
"jsx": "react-jsxdev",
"lib": ["ES2022"],
// weaken strictness from preset
"importsNotUsedAsValues": "remove",
"noUncheckedIndexedAccess": false,
"noUnusedParameters": false,
"useUnknownInCatchVariables": false,
"target": "ES2022",
// ideally turn off only during dev, or on a per-file basis
"noUnusedLocals": false,
},
"exclude": ["node_modules"],
"include": ["src/**/*.ts"],
}
@@ -0,0 +1,30 @@
import {defineConfig} from 'tsup';
export default defineConfig({
entry: ['./src/index.ts'],
outDir: './dist',
external: [],
splitting: false,
sourcemap: false,
dts: false,
bundle: true,
format: 'cjs',
platform: 'node',
target: 'es2022',
banner: {
js: `#!/usr/bin/env node
/**
* 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.
*
* @lightSyntaxTransform
* @noflow
* @nolint
* @preventMunge
* @preserve-invariant-messages
*/`,
},
});
+14 -2
View File
@@ -62,9 +62,15 @@ async function main() {
.option('tag', {
description: 'Tag to publish to npm',
type: 'choices',
choices: ['experimental', 'beta'],
choices: ['experimental', 'beta', 'rc'],
default: 'experimental',
})
.option('tag-version', {
description:
'Optional tag version to append to tag name, eg `1` becomes 0.0.0-rc.1',
type: 'number',
default: null,
})
.option('version-name', {
description: 'Version name',
type: 'string',
@@ -133,7 +139,13 @@ async function main() {
files: {exclude: ['.DS_Store']},
});
const truncatedHash = hash.slice(0, 7);
const newVersion = `${argv.versionName}-${argv.tag}-${truncatedHash}-${dateString}`;
let newVersion =
argv.tagVersion == null || argv.tagVersion === ''
? `${argv.versionName}-${argv.tag}`
: `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
if (argv.tag === 'experimental' || argv.tag === 'beta') {
newVersion = `${newVersion}-${truncatedHash}-${dateString}`;
}
for (const pkgName of pkgNames) {
const pkgDir = path.resolve(__dirname, `../../packages/${pkgName}`);
+922 -12
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
v18
v20.19.0
+1 -1
View File
@@ -1 +1 @@
v18
v20.19.0
+2 -1
View File
@@ -23,6 +23,7 @@
"browserslist": "^4.18.1",
"busboy": "^1.6.0",
"camelcase": "^6.2.1",
"canvas": "^3.1.0",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"compression": "^1.7.4",
"concurrently": "^7.3.0",
@@ -65,7 +66,7 @@
"webpack-manifest-plugin": "^4.0.2"
},
"devDependencies": {
"@playwright/test": "^1.49.1"
"@playwright/test": "^1.51.1"
},
"scripts": {
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
+7 -1
View File
@@ -15,6 +15,8 @@ import {Client} from './Client.js';
import {Note} from './cjs/Note.js';
import {GenerateImage} from './GenerateImage.js';
import {like, greet, increment} from './actions.js';
import {getServerState} from './ServerState.js';
@@ -41,6 +43,7 @@ export default async function App({prerender}) {
const todos = await res.json();
const dedupedChild = <ServerComponent />;
const message = getServerState();
return (
<html lang="en">
<head>
@@ -55,7 +58,7 @@ export default async function App({prerender}) {
) : (
<meta content="when not prerendering we render this meta tag. When prerendering you will expect to see this tag and the one with data-testid=prerendered because we SSR one and hydrate the other" />
)}
<h1>{getServerState()}</h1>
<h1>{message}</h1>
<React.Suspense fallback={null}>
<div data-testid="promise-as-a-child-test">
Promise as a child hydrates without errors: {promisedText}
@@ -79,6 +82,9 @@ export default async function App({prerender}) {
<div>
loaded statically: <Dynamic />
</div>
<div>
<GenerateImage message={message} />
</div>
<Client />
<Note />
<Foo>{dedupedChild}</Foo>
+19
View File
@@ -0,0 +1,19 @@
import * as React from 'react';
import {createCanvas} from 'canvas';
export async function GenerateImage({message}) {
// Generate an image using an image library
const canvas = createCanvas(200, 70);
const ctx = canvas.getContext('2d');
ctx.font = '20px Impact';
ctx.rotate(-0.1);
ctx.fillText(message, 10, 50);
// Rasterize into a Blob with a mime type
const type = 'image/png';
const blob = new Blob([canvas.toBuffer(type)], {type});
// Just pass it to React
return <img src={blob} />;
}
+236 -31
View File
@@ -2748,12 +2748,12 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@playwright/test@^1.49.1":
version "1.49.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.1.tgz#55fa360658b3187bfb6371e2f8a64f50ef80c827"
integrity sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==
"@playwright/test@^1.51.1":
version "1.51.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.1.tgz#75357d513221a7be0baad75f01e966baf9c41a2e"
integrity sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==
dependencies:
playwright "1.49.1"
playwright "1.51.1"
"@pmmmwh/react-refresh-webpack-plugin@0.5.15":
version "0.5.15"
@@ -3747,6 +3747,11 @@ balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
@@ -3757,6 +3762,15 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9"
integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==
bl@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
readable-stream "^3.4.0"
body-parser@^1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
@@ -3853,6 +3867,14 @@ buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
busboy@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
@@ -3930,20 +3952,18 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000888, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001373:
version "1.0.30001457"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz"
integrity sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000888, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001373, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001646:
version "1.0.30001713"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz"
integrity sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==
caniuse-lite@^1.0.30001503:
version "1.0.30001505"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz#10a343e49d31cbbfdae298ef73cb0a9f46670dc5"
integrity sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==
caniuse-lite@^1.0.30001646:
version "1.0.30001651"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138"
integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==
canvas@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/canvas/-/canvas-3.1.0.tgz#6cdf094b859fef8e39b0e2c386728a376f1727b2"
integrity sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==
dependencies:
node-addon-api "^7.0.0"
prebuild-install "^7.1.1"
case-sensitive-paths-webpack-plugin@^2.4.0:
version "2.4.0"
@@ -4007,6 +4027,11 @@ chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chrome-trace-event@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@@ -4501,11 +4526,23 @@ decimal.js@^10.2.1:
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.0.tgz#97a7448873b01e92e5ff9117d89a7bca8e63e0fe"
integrity sha512-Nv6ENEzyPQ6AItkGwLE2PGKinZZ9g59vSh2BeH6NqPu0OTKZ5ruJsVqh/orbAnqXc9pBbgXAIrc2EyaCj8NpGg==
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
dependencies:
mimic-response "^3.1.0"
dedent@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
@@ -4559,6 +4596,11 @@ destroy@1.2.0:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
detect-libc@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
@@ -4745,6 +4787,13 @@ emojis-list@^3.0.0:
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
enhanced-resolve@^5.17.0:
version "5.17.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15"
@@ -4989,6 +5038,11 @@ exit@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
expect@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74"
@@ -5159,6 +5213,11 @@ fraction.js@^4.2.0:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs-extra@^10.0.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
@@ -5265,6 +5324,11 @@ get-symbol-description@^1.0.2:
es-errors "^1.3.0"
get-intrinsic "^1.2.4"
github-from-package@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -5572,6 +5636,11 @@ identity-obj-proxy@^3.0.0:
dependencies:
harmony-reflect "^1.4.6"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
@@ -5627,7 +5696,7 @@ inherits@2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
inherits@2.0.4:
inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -5636,6 +5705,11 @@ ini@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
internal-slot@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
@@ -6699,6 +6773,11 @@ mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
@@ -6735,11 +6814,21 @@ minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
minimist@^1.2.0, minimist@^1.2.3:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
@@ -6778,6 +6867,11 @@ nanoid@^3.3.7:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
napi-build-utils@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e"
integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -6800,6 +6894,18 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
node-abi@^3.3.0:
version "3.74.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.74.0.tgz#5bfb4424264eaeb91432d2adb9da23c63a301ed0"
integrity sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==
dependencies:
semver "^7.3.5"
node-addon-api@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -6959,7 +7065,7 @@ on-headers@~1.0.2:
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
once@^1.3.0:
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
@@ -7178,17 +7284,17 @@ pkg-up@^3.1.0:
dependencies:
find-up "^3.0.0"
playwright-core@1.49.1:
version "1.49.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.1.tgz#32c62f046e950f586ff9e35ed490a424f2248015"
integrity sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==
playwright-core@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.1.tgz#d57f0393e02416f32a47cf82b27533656a8acce1"
integrity sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==
playwright@1.49.1:
version "1.49.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.1.tgz#830266dbca3008022afa7b4783565db9944ded7c"
integrity sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==
playwright@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.1.tgz#ae1467ee318083968ad28d6990db59f47a55390f"
integrity sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==
dependencies:
playwright-core "1.49.1"
playwright-core "1.51.1"
optionalDependencies:
fsevents "2.3.2"
@@ -7781,6 +7887,24 @@ postcss@^8.4.23:
picocolors "^1.0.1"
source-map-js "^1.2.0"
prebuild-install@^7.1.1:
version "7.1.3"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec"
integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==
dependencies:
detect-libc "^2.0.0"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp-classic "^0.5.3"
napi-build-utils "^2.0.0"
node-abi "^3.3.0"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^4.0.0"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -7837,6 +7961,14 @@ pstree.remy@^1.1.8:
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
pump@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
punycode@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
@@ -7888,6 +8020,16 @@ raw-body@2.5.1:
iconv-lite "0.4.24"
unpipe "1.0.0"
rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
dependencies:
deep-extend "^0.6.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-dev-utils@^12.0.1:
version "12.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73"
@@ -7966,6 +8108,15 @@ read-cache@^1.0.0:
dependencies:
pify "^2.3.0"
readable-stream@^3.1.1, readable-stream@^3.4.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -8191,7 +8342,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@^5.1.0:
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -8368,6 +8519,20 @@ signal-exit@^4.0.1:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
dependencies:
decompress-response "^6.0.0"
once "^1.3.1"
simple-concat "^1.0.0"
simple-update-notifier@^1.0.7:
version "1.1.0"
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82"
@@ -8558,6 +8723,13 @@ string.prototype.trimstart@^1.0.3, string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@@ -8601,6 +8773,11 @@ strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
style-loader@^3.3.1:
version "3.3.4"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.4.tgz#f30f786c36db03a45cbd55b6a70d930c479090e7"
@@ -8741,6 +8918,27 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
tar-fs@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.2.tgz#425f154f3404cb16cb8ff6e671d45ab2ed9596c5"
integrity sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.1.4"
tar-stream@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
dependencies:
bl "^4.0.3"
end-of-stream "^1.4.1"
fs-constants "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.1.1"
terminal-link@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
@@ -8867,6 +9065,13 @@ tslib@^2.0.3, tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
dependencies:
safe-buffer "^5.0.1"
type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@@ -9041,7 +9246,7 @@ url-parse@^1.5.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
util-deprecate@^1.0.2:
util-deprecate@^1.0.1, util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
@@ -20,4 +20,11 @@
border: 0px;
border-radius: 5px;
padding: 10px;
}
.portal {
position: fixed;
top: 10px;
left: 360px;
border: 1px solid #ccc;
}
@@ -1,13 +1,17 @@
import React, {
unstable_addTransitionType as addTransitionType,
unstable_ViewTransition as ViewTransition,
unstable_Activity as Activity,
unstable_useSwipeTransition as useSwipeTransition,
useLayoutEffect,
useEffect,
useState,
useId,
useOptimistic,
startTransition,
} from 'react';
import {createPortal} from 'react-dom';
import SwipeRecognizer from './SwipeRecognizer';
import './Page.css';
@@ -37,6 +41,12 @@ function Component() {
transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
}>
<p className="roboto-font">Slide In from Left, Slide Out to Right</p>
<p>
<img
src="https://react.dev/_next/image?url=%2Fimages%2Fteam%2Fsebmarkbage.jpg&w=3840&q=75"
width="300"
/>
</p>
</ViewTransition>
);
}
@@ -47,7 +57,12 @@ function Id() {
}
export default function Page({url, navigate}) {
const [renderedUrl, startGesture] = useSwipeTransition('/?a', url, '/?b');
const [renderedUrl, optimisticNavigate] = useOptimistic(
url,
(state, direction) => {
return direction === 'left' ? '/?a' : '/?b';
}
);
const show = renderedUrl === '/?b';
function onTransition(viewTransition, types) {
const keyframes = [
@@ -79,16 +94,40 @@ export default function Page({url, navigate}) {
// });
}, [show]);
const [showModal, setShowModal] = useState(false);
const portal = showModal ? (
createPortal(
<div className="portal">
Portal: {!show ? 'A' : 'B'}
<ViewTransition>
<div>{!show ? 'A' : 'B'}</div>
</ViewTransition>
</div>,
document.body
)
) : (
<button onClick={() => startTransition(() => setShowModal(true))}>
Show Modal
</button>
);
const exclamation = (
<ViewTransition name="exclamation" onShare={onTransition}>
<span>!</span>
<span>
<div>!</div>
</span>
</ViewTransition>
);
return (
<div className="swipe-recognizer">
<SwipeRecognizer
action={swipeAction}
gesture={startGesture}
gesture={direction => {
addTransitionType(
direction === 'left' ? 'navigation-forward' : 'navigation-back'
);
optimisticNavigate(direction);
}}
direction={show ? 'left' : 'right'}>
<button
className="button"
@@ -153,6 +192,7 @@ export default function Page({url, navigate}) {
<p>content</p>
<p>out</p>
<p>of</p>
{portal}
<p>the</p>
<p>viewport</p>
{show ? <Component /> : null}
@@ -1,4 +1,9 @@
import React, {useRef, useEffect, startTransition} from 'react';
import React, {
useRef,
useEffect,
startTransition,
unstable_startGestureTransition as startGestureTransition,
} from 'react';
// Example of a Component that can recognize swipe gestures using a ScrollTimeline
// without scrolling its own content. Allowing it to be used as an inert gesture
@@ -28,9 +33,21 @@ export default function SwipeRecognizer({
source: scrollRef.current,
axis: axis,
});
activeGesture.current = gesture(scrollTimeline, {
range: [0, direction === 'left' || direction === 'up' ? 100 : 0, 100],
});
activeGesture.current = startGestureTransition(
scrollTimeline,
() => {
gesture(direction);
},
direction === 'left' || direction === 'up'
? {
rangeStart: 100,
rangeEnd: 0,
}
: {
rangeStart: 0,
rangeEnd: 100,
}
);
}
function onScrollEnd() {
let changed;
@@ -20,6 +20,19 @@ yarn add eslint-plugin-react-hooks --dev
### Flat Config (eslint.config.js|ts)
#### >= 6.0.0
For users of 6.0 and beyond, simply add the `recommended` config.
```js
import * as reactHooks from 'eslint-plugin-react-hooks';
export default [
// ...
reactHooks.configs.recommended,
];
```
#### 5.2.0
For users of 5.2.0 (the first version with flat config support), add the `recommended-latest` config.
@@ -94,36 +94,8 @@ const tests: CompilerTestCases = {
}
`,
},
{
// Don't report the issue if Flow already has
name: '[InvalidInput] Ref access during render',
code: normalizeIndent`
function Component(props) {
const ref = useRef(null);
// $FlowFixMe[react-rule-unsafe-ref]
const value = ref.current;
return value;
}
`,
},
],
invalid: [
{
name: '[InvalidInput] Ref access during render',
code: normalizeIndent`
function Component(props) {
const ref = useRef(null);
const value = ref.current;
return value;
}
`,
errors: [
{
message:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
},
],
},
{
name: 'Reportable levels can be configured',
options: [{reportableLevels: new Set([ErrorSeverity.Todo])}],
@@ -107,6 +107,9 @@ const COMPILER_OPTIONS: Partial<PluginOptions> = {
panicThreshold: 'none',
// Don't emit errors on Flow suppressions--Flow already gave a signal
flowSuppressions: false,
environment: validateEnvironmentConfig({
validateRefAccessDuringRender: false,
}),
};
const rule: Rule.RuleModule = {
@@ -151,10 +154,14 @@ const rule: Rule.RuleModule = {
}
let shouldReportUnusedOptOutDirective = true;
const options: PluginOptions = {
...parsePluginOptions(userOpts),
const options: PluginOptions = parsePluginOptions({
...COMPILER_OPTIONS,
};
...userOpts,
environment: {
...COMPILER_OPTIONS.environment,
...userOpts.environment,
},
});
const userLogger: Logger | null = options.logger;
options.logger = {
logEvent: (eventFilename, event): void => {
+3 -3
View File
@@ -24,10 +24,10 @@
"dependencies": {
"art": "^0.10.1",
"create-react-class": "^15.6.2",
"scheduler": "^0.25.0"
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.0.0"
"react": "^19.1.0"
},
"files": [
"LICENSE",
@@ -38,4 +38,4 @@
"Rectangle.js",
"Wedge.js"
]
}
}
+10 -10
View File
@@ -560,15 +560,7 @@ export function createViewTransitionInstance(
export type GestureTimeline = null;
export function getCurrentGestureOffset(provider: GestureTimeline): number {
throw new Error('useSwipeTransition is not yet supported in react-art.');
}
export function subscribeToGestureDirection(
provider: GestureTimeline,
currentOffset: number,
directionCallback: (direction: boolean) => void,
): () => void {
throw new Error('useSwipeTransition is not yet supported in react-art.');
throw new Error('startGestureTransition is not yet supported in react-art.');
}
export function clearContainer(container) {
@@ -604,6 +596,14 @@ export function maySuspendCommit(type, props) {
return false;
}
export function maySuspendCommitOnUpdate(type, oldProps, newProps) {
return false;
}
export function maySuspendCommitInSyncRender(type, props) {
return false;
}
export function preloadInstance(type, props) {
// Return true to indicate it's already loaded
return true;
@@ -611,7 +611,7 @@ export function preloadInstance(type, props) {
export function startSuspendingCommit() {}
export function suspendInstance(type, props) {}
export function suspendInstance(instance, type, props) {}
export function suspendOnActiveViewTransition(container) {}
-22
View File
@@ -14,7 +14,6 @@ import type {
Usable,
Thenable,
ReactDebugInfo,
StartGesture,
} from 'shared/ReactTypes';
import type {
ContextDependency,
@@ -132,9 +131,6 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
if (typeof Dispatcher.useEffectEvent === 'function') {
Dispatcher.useEffectEvent((args: empty) => {});
}
if (typeof Dispatcher.useSwipeTransition === 'function') {
Dispatcher.useSwipeTransition(null, null, null);
}
} finally {
readHookLog = hookLog;
hookLog = [];
@@ -753,23 +749,6 @@ function useEffectEvent<Args, F: (...Array<Args>) => mixed>(callback: F): F {
return callback;
}
function useSwipeTransition<T>(
previous: T,
current: T,
next: T,
): [T, StartGesture] {
nextHook();
hookLog.push({
displayName: null,
primitive: 'SwipeTransition',
stackError: new Error(),
value: current,
debugInfo: null,
dispatcherHookName: 'SwipeTransition',
});
return [current, () => () => {}];
}
const Dispatcher: DispatcherType = {
readContext,
@@ -796,7 +775,6 @@ const Dispatcher: DispatcherType = {
useMemoCache,
useCacheRefresh,
useEffectEvent,
useSwipeTransition,
};
// create a proxy to throw a custom error
@@ -1,6 +1,6 @@
/* global chrome */
import {normalizeUrl} from 'react-devtools-shared/src/utils';
import {normalizeUrlIfValid} from 'react-devtools-shared/src/utils';
import {__DEBUG__} from 'react-devtools-shared/src/constants';
let debugIDCounter = 0;
@@ -117,7 +117,7 @@ async function fetchFileWithCaching(url: string): Promise<string> {
chrome.devtools.inspectedWindow.getResources(r => resolve(r)),
);
const normalizedReferenceURL = normalizeUrl(url);
const normalizedReferenceURL = normalizeUrlIfValid(url);
const resource = resources.find(r => r.url === normalizedReferenceURL);
if (resource != null) {
+6 -1
View File
@@ -16,6 +16,7 @@ import {
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
} from 'react-devtools-shared/src/constants';
import {logEvent} from 'react-devtools-shared/src/Logger';
import {normalizeUrlIfValid} from 'react-devtools-shared/src/utils';
import {
setBrowserSelectionFromReact,
@@ -128,7 +129,11 @@ function createBridgeAndStore() {
: source;
// We use 1-based line and column, Chrome expects them 0-based.
chrome.devtools.panels.openResource(sourceURL, line - 1, column - 1);
chrome.devtools.panels.openResource(
normalizeUrlIfValid(sourceURL),
line - 1,
column - 1,
);
};
// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
+1 -1
View File
@@ -33,7 +33,7 @@
"@babel/preset-env": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@playwright/test": "^1.16.3",
"@playwright/test": "^1.51.1",
"babel-core": "^7.0.0-bridge",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.4",
@@ -248,7 +248,7 @@ function createVirtualInstance(
type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance;
type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
type getTypeSymbolType = (type: any) => symbol | number;
type getTypeSymbolType = (type: any) => symbol | string | number;
type ReactPriorityLevelsType = {
ImmediatePriority: number,
@@ -541,13 +541,12 @@ export function getInternalReactConstants(version: string): {
// End of copied code.
// **********************************************************
function getTypeSymbol(type: any): symbol | number {
function getTypeSymbol(type: any): symbol | string | number {
const symbolOrNumber =
typeof type === 'object' && type !== null ? type.$$typeof : type;
return typeof symbolOrNumber === 'symbol'
? // $FlowFixMe[incompatible-return] `toString()` doesn't match the type signature?
symbolOrNumber.toString()
? symbolOrNumber.toString()
: symbolOrNumber;
}
@@ -808,6 +807,27 @@ function getPublicInstance(instance: HostInstance): HostInstance {
return instance;
}
function getNativeTag(instance: HostInstance): number | null {
if (typeof instance !== 'object' || instance === null) {
return null;
}
// Modern. Fabric.
if (
instance.canonical != null &&
typeof instance.canonical.nativeTag === 'number'
) {
return instance.canonical.nativeTag;
}
// Legacy. Paper.
if (typeof instance._nativeTag === 'number') {
return instance._nativeTag;
}
return null;
}
function aquireHostInstance(
nearestInstance: DevToolsInstance,
hostInstance: HostInstance,
@@ -3324,13 +3344,31 @@ export function attach(
fiberInstance.firstChild = null;
}
try {
if (nextFiber.tag === HostHoistable) {
if (
nextFiber.tag === HostHoistable &&
prevFiber.memoizedState !== nextFiber.memoizedState
) {
const nearestInstance = reconcilingParent;
if (nearestInstance === null) {
throw new Error('Did not expect a host hoistable to be the root');
}
releaseHostResource(nearestInstance, prevFiber.memoizedState);
aquireHostResource(nearestInstance, nextFiber.memoizedState);
} else if (
(nextFiber.tag === HostComponent ||
nextFiber.tag === HostText ||
nextFiber.tag === HostSingleton) &&
prevFiber.stateNode !== nextFiber.stateNode
) {
// In persistent mode, it's possible for the stateNode to update with
// a new clone. In that case we need to release the old one and aquire
// new one instead.
const nearestInstance = reconcilingParent;
if (nearestInstance === null) {
throw new Error('Did not expect a host hoistable to be the root');
}
releaseHostInstance(nearestInstance, prevFiber.stateNode);
aquireHostInstance(nearestInstance, nextFiber.stateNode);
}
const isSuspense = nextFiber.tag === SuspenseComponent;
@@ -4298,6 +4336,11 @@ export function attach(
componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);
}
let nativeTag = null;
if (elementType === ElementTypeHostComponent) {
nativeTag = getNativeTag(fiber.stateNode);
}
return {
id: fiberInstance.id,
@@ -4364,6 +4407,8 @@ export function attach(
rendererVersion: renderer.version,
plugins,
nativeTag,
};
}
@@ -4457,6 +4502,8 @@ export function attach(
rendererVersion: renderer.version,
plugins,
nativeTag: null,
};
}
@@ -859,6 +859,8 @@ export function attach(
plugins: {
stylex: null,
},
nativeTag: null,
};
}
+3
View File
@@ -294,6 +294,9 @@ export type InspectedElement = {
// UI plugins/visualizations for the inspected element.
plugins: Plugins,
// React Native only.
nativeTag: number | null,
};
export const InspectElementErrorType = 'error';
+2
View File
@@ -239,6 +239,7 @@ export function convertInspectedElementBackendToFrontend(
key,
errors,
warnings,
nativeTag,
} = inspectedElementBackend;
const inspectedElement: InspectedElementFrontend = {
@@ -273,6 +274,7 @@ export function convertInspectedElementBackendToFrontend(
state: hydrateHelper(state),
errors,
warnings,
nativeTag,
};
return inspectedElement;
@@ -27,6 +27,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-background-selected': '#0088fa',
'--color-button-background': '#ffffff',
'--color-button-background-focus': '#ededed',
'--color-button-background-hover': 'rgba(0, 0, 0, 0.2)',
'--color-button': '#5f6673',
'--color-button-disabled': '#cfd1d5',
'--color-button-active': '#0088fa',
@@ -174,6 +175,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-background-selected': '#178fb9',
'--color-button-background': '#282c34',
'--color-button-background-focus': '#3d424a',
'--color-button-background-hover': 'rgba(255, 255, 255, 0.2)',
'--color-button': '#afb3b9',
'--color-button-active': '#61dafb',
'--color-button-disabled': '#4f5766',
@@ -19,7 +19,6 @@ import {
} from 'react-devtools-shared/src/storage';
import InspectedElementErrorBoundary from './InspectedElementErrorBoundary';
import InspectedElement from './InspectedElement';
import {InspectedElementContextController} from './InspectedElementContext';
import {ModalDialog} from '../ModalDialog';
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
import {NativeStyleContextController} from './NativeStyleEditor/context';
@@ -162,9 +161,7 @@ function Components(_: {}) {
<div className={styles.InspectedElementWrapper}>
<NativeStyleContextController>
<InspectedElementErrorBoundary>
<InspectedElementContextController>
<InspectedElement />
</InspectedElementContextController>
<InspectedElement />
</InspectedElementErrorBoundary>
</NativeStyleContextController>
</div>
@@ -11,21 +11,25 @@ import * as React from 'react';
import Badge from './Badge';
import ForgetBadge from './ForgetBadge';
import NativeTagBadge from './NativeTagBadge';
import styles from './InspectedElementBadges.css';
type Props = {
hocDisplayNames: null | Array<string>,
compiledWithForget: boolean,
nativeTag: number | null,
};
export default function InspectedElementBadges({
hocDisplayNames,
compiledWithForget,
nativeTag,
}: Props): React.Node {
if (
!compiledWithForget &&
(hocDisplayNames == null || hocDisplayNames.length === 0)
(hocDisplayNames == null || hocDisplayNames.length === 0) &&
nativeTag === null
) {
return null;
}
@@ -33,6 +37,7 @@ export default function InspectedElementBadges({
return (
<div className={styles.Root}>
{compiledWithForget && <ForgetBadge indexable={false} />}
{nativeTag !== null && <NativeTagBadge nativeTag={nativeTag} />}
{hocDisplayNames !== null &&
hocDisplayNames.map(hocDisplayName => (
@@ -54,8 +54,14 @@ export default function InspectedElementView({
toggleParseHookNames,
symbolicatedSourcePromise,
}: Props): React.Node {
const {owners, rendererPackageName, rendererVersion, rootType, source} =
inspectedElement;
const {
owners,
rendererPackageName,
rendererVersion,
rootType,
source,
nativeTag,
} = inspectedElement;
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
@@ -75,6 +81,7 @@ export default function InspectedElementView({
<InspectedElementBadges
hocDisplayNames={element.hocDisplayNames}
compiledWithForget={element.compiledWithForget}
nativeTag={nativeTag}
/>
</div>
@@ -0,0 +1,11 @@
.Toggle {
display: flex;
}
.Toggle > span { /* targets .ToggleContent */
padding: 0;
}
.Badge {
cursor: help;
}
@@ -0,0 +1,31 @@
/**
* 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 * as React from 'react';
import Badge from './Badge';
import Toggle from '../Toggle';
import styles from './NativeTagBadge.css';
type Props = {
nativeTag: number,
};
const noop = () => {};
const title =
'Unique identifier for the corresponding native component. React Native only.';
export default function NativeTagBadge({nativeTag}: Props): React.Node {
return (
<Toggle onChange={noop} className={styles.Toggle} title={title}>
<Badge className={styles.Badge}>Tag {nativeTag}</Badge>
</Toggle>
);
}
+39 -34
View File
@@ -28,6 +28,7 @@ import {SettingsContextController} from './Settings/SettingsContext';
import {TreeContextController} from './Components/TreeContext';
import ViewElementSourceContext from './Components/ViewElementSourceContext';
import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext';
import {InspectedElementContextController} from './Components/InspectedElementContext';
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
import {ProfilerContextController} from './Profiler/ProfilerContext';
import {TimelineContextController} from 'react-devtools-timeline/src/TimelineContext';
@@ -276,43 +277,47 @@ export default function DevTools({
<TreeContextController>
<ProfilerContextController>
<TimelineContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
<InspectedElementContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={
componentsPortalContainer
}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
/>
</div>
</div>
</ThemeProvider>
</ThemeProvider>
</InspectedElementContextController>
</TimelineContextController>
</ProfilerContextController>
</TreeContextController>
@@ -0,0 +1,59 @@
.LoadHookNamesToggle,
.ToggleError {
padding: 2px;
background: none;
border: none;
cursor: pointer;
position: relative;
bottom: -0.2em;
margin-block: -1em;
}
.ToggleError {
color: var(--color-error-text);
}
.Hook {
list-style-type: none;
margin: 0;
padding-left: 0.5rem;
line-height: 1.125rem;
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
}
.Hook .Hook {
padding-left: 1rem;
}
.Name {
color: var(--color-dim);
flex: 0 0 auto;
cursor: default;
}
.PrimitiveHookName {
color: var(--color-text);
flex: 0 0 auto;
cursor: default;
}
.Name:after {
color: var(--color-text);
content: ': ';
margin-right: 0.5rem;
}
.PrimitiveHookNumber {
background-color: var(--color-primitive-hook-badge-background);
color: var(--color-primitive-hook-badge-text);
font-size: var(--font-size-monospace-small);
margin-right: 0.25rem;
border-radius: 0.125rem;
padding: 0.125rem 0.25rem;
}
.HookName {
color: var(--color-component-name);
}
@@ -0,0 +1,207 @@
/**
* 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 * as React from 'react';
import {
useContext,
useMemo,
useCallback,
memo,
useState,
useEffect,
} from 'react';
import styles from './HookChangeSummary.css';
import ButtonIcon from '../ButtonIcon';
import {InspectedElementContext} from '../Components/InspectedElementContext';
import {StoreContext} from '../context';
import {
getAlreadyLoadedHookNames,
getHookSourceLocationKey,
} from 'react-devtools-shared/src/hookNamesCache';
import Toggle from '../Toggle';
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
import type {ChangeDescription} from './types';
// $FlowFixMe: Flow doesn't know about Intl.ListFormat
const hookListFormatter = new Intl.ListFormat('en', {
style: 'long',
type: 'conjunction',
});
type HookProps = {
hook: HooksNode,
hookNames: Map<string, string> | null,
};
const Hook: React.AbstractComponent<HookProps> = memo(({hook, hookNames}) => {
const hookSource = hook.hookSource;
const hookName = useMemo(() => {
if (!hookSource || !hookNames) return null;
const key = getHookSourceLocationKey(hookSource);
return hookNames.get(key) || null;
}, [hookSource, hookNames]);
return (
<ul className={styles.Hook}>
<li>
{hook.id !== null && (
<span className={styles.PrimitiveHookNumber}>
{String(hook.id + 1)}
</span>
)}
<span
className={hook.id !== null ? styles.PrimitiveHookName : styles.Name}>
{hook.name}
{hookName && <span className={styles.HookName}>({hookName})</span>}
</span>
{hook.subHooks?.map((subHook, index) => (
<Hook key={hook.id} hook={subHook} hookNames={hookNames} />
))}
</li>
</ul>
);
});
const shouldKeepHook = (
hook: HooksNode,
hooksArray: Array<number>,
): boolean => {
if (hook.id !== null && hooksArray.includes(hook.id)) {
return true;
}
const subHooks = hook.subHooks;
if (subHooks == null) {
return false;
}
return subHooks.some(subHook => shouldKeepHook(subHook, hooksArray));
};
const filterHooks = (
hook: HooksNode,
hooksArray: Array<number>,
): HooksNode | null => {
if (!shouldKeepHook(hook, hooksArray)) {
return null;
}
const subHooks = hook.subHooks;
if (subHooks == null) {
return hook;
}
const filteredSubHooks = subHooks
.map(subHook => filterHooks(subHook, hooksArray))
.filter(Boolean);
return filteredSubHooks.length > 0
? {...hook, subHooks: filteredSubHooks}
: hook;
};
type Props = {|
fiberID: number,
hooks: $PropertyType<ChangeDescription, 'hooks'>,
state: $PropertyType<ChangeDescription, 'state'>,
displayMode?: 'detailed' | 'compact',
|};
const HookChangeSummary: React.AbstractComponent<Props> = memo(
({hooks, fiberID, state, displayMode = 'detailed'}: Props) => {
const {parseHookNames, toggleParseHookNames, inspectedElement} = useContext(
InspectedElementContext,
);
const store = useContext(StoreContext);
const [parseHookNamesOptimistic, setParseHookNamesOptimistic] =
useState<boolean>(parseHookNames);
useEffect(() => {
setParseHookNamesOptimistic(parseHookNames);
}, [inspectedElement?.id, parseHookNames]);
const handleOnChange = useCallback(() => {
setParseHookNamesOptimistic(!parseHookNames);
toggleParseHookNames();
}, [toggleParseHookNames, parseHookNames]);
const element = fiberID !== null ? store.getElementByID(fiberID) : null;
const hookNames =
element != null ? getAlreadyLoadedHookNames(element) : null;
const filteredHooks = useMemo(() => {
if (!hooks || !inspectedElement?.hooks) return null;
return inspectedElement.hooks
.map(hook => filterHooks(hook, hooks))
.filter(Boolean);
}, [inspectedElement?.hooks, hooks]);
const hookParsingFailed = parseHookNames && hookNames === null;
if (!hooks?.length) {
return <span>No hooks changed</span>;
}
if (
inspectedElement?.id !== element?.id ||
filteredHooks?.length !== hooks.length ||
displayMode === 'compact'
) {
const hookIds = hooks.map(hookId => String(hookId + 1));
const hookWord = hookIds.length === 1 ? '• Hook' : '• Hooks';
return (
<span>
{hookWord} {hookListFormatter.format(hookIds)} changed
</span>
);
}
let toggleTitle: string;
if (hookParsingFailed) {
toggleTitle = 'Hook parsing failed';
} else if (parseHookNamesOptimistic) {
toggleTitle = 'Parsing hook names ...';
} else {
toggleTitle = 'Parse hook names (may be slow)';
}
if (filteredHooks == null) {
return null;
}
return (
<div>
{filteredHooks.length > 1 ? '• Hooks changed:' : '• Hook changed:'}
{(!parseHookNames || hookParsingFailed) && (
<Toggle
className={
hookParsingFailed
? styles.ToggleError
: styles.LoadHookNamesToggle
}
isChecked={parseHookNamesOptimistic}
isDisabled={parseHookNamesOptimistic || hookParsingFailed}
onChange={handleOnChange}
title={toggleTitle}>
<ButtonIcon type="parse-hook-names" />
</Toggle>
)}
{filteredHooks.map(hook => (
<Hook
key={`${inspectedElement?.id ?? 'unknown'}-${hook.id}`}
hook={hook}
hookNames={hookNames}
/>
))}
</div>
);
},
);
export default HookChangeSummary;
@@ -95,7 +95,7 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
<div className={styles.Content}>
{renderDurationInfo || <div>Did not client render.</div>}
<WhatChanged fiberID={id} />
<WhatChanged fiberID={id} displayMode="compact" />
</div>
</div>
</Fragment>
@@ -14,30 +14,17 @@ import {ProfilerContext} from './ProfilerContext';
import {StoreContext} from '../context';
import styles from './WhatChanged.css';
function hookIndicesToString(indices: Array<number>): string {
// This is debatable but I think 1-based might ake for a nicer UX.
const numbers = indices.map(value => value + 1);
switch (numbers.length) {
case 0:
return 'No hooks changed';
case 1:
return `Hook ${numbers[0]} changed`;
case 2:
return `Hooks ${numbers[0]} and ${numbers[1]} changed`;
default:
return `Hooks ${numbers.slice(0, numbers.length - 1).join(', ')} and ${
numbers[numbers.length - 1]
} changed`;
}
}
import HookChangeSummary from './HookChangeSummary';
type Props = {
fiberID: number,
displayMode?: 'detailed' | 'compact',
};
export default function WhatChanged({fiberID}: Props): React.Node {
export default function WhatChanged({
fiberID,
displayMode = 'detailed',
}: Props): React.Node {
const {profilerStore} = useContext(StoreContext);
const {rootID, selectedCommitIndex} = useContext(ProfilerContext);
@@ -106,7 +93,12 @@ export default function WhatChanged({fiberID}: Props): React.Node {
if (Array.isArray(hooks)) {
changes.push(
<div key="hooks" className={styles.Item}>
{hookIndicesToString(hooks)}
<HookChangeSummary
hooks={hooks}
fiberID={fiberID}
state={state}
displayMode={displayMode}
/>
</div>,
);
} else {
@@ -20,10 +20,17 @@
background: var(--color-button-background);
color: var(--color-button);
}
.ToggleOff:hover {
color: var(--color-button-hover);
}
.ToggleOn:hover,
.ToggleOff:hover {
background-color: var(--color-button-background-hover);
}
.ToggleOn,
.ToggleOn:active {
color: var(--color-button-active);
+3
View File
@@ -259,6 +259,9 @@ export type InspectedElement = {
// UI plugins/visualizations for the inspected element.
plugins: Plugins,
// React Native only.
nativeTag: number | null,
};
// TODO: Add profiling type
+8
View File
@@ -72,6 +72,14 @@ export function hasAlreadyLoadedHookNames(element: Element): boolean {
return record != null && record.status === Resolved;
}
export function getAlreadyLoadedHookNames(element: Element): HookNames | null {
const record = map.get(element);
if (record != null && record.status === Resolved) {
return record.value;
}
return null;
}
export function loadHookNames(
element: Element,
hooksTree: HooksTree,
+1 -3
View File
@@ -7,7 +7,6 @@
* @flow
*/
import {normalizeUrl} from 'react-devtools-shared/src/utils';
import SourceMapConsumer from 'react-devtools-shared/src/hooks/SourceMapConsumer';
import type {Source} from 'react-devtools-shared/src/shared/types';
@@ -91,9 +90,8 @@ export async function symbolicateSource(
try {
// sourceMapURL = https://react.dev/script.js.map
void new URL(possiblyURL); // test if it is a valid URL
const normalizedURL = normalizeUrl(possiblyURL);
return {sourceURL: normalizedURL, line, column};
return {sourceURL: possiblyURL, line, column};
} catch (e) {
// This is not valid URL
if (
+11 -3
View File
@@ -996,9 +996,17 @@ export function backendToFrontendSerializedElementMapper(
};
}
// Chrome normalizes urls like webpack-internals:// but new URL don't, so cannot use new URL here.
export function normalizeUrl(url: string): string {
return url.replace('/./', '/');
/**
* Should be used when treating url as a Chrome Resource URL.
*/
export function normalizeUrlIfValid(url: string): string {
try {
// TODO: Chrome will use the basepath to create a Resource URL.
return new URL(url).toString();
} catch {
// Giving up if it's not a valid URL without basepath
return url;
}
}
export function getIsReloadAndProfileSupported(): boolean {
+108 -2
View File
@@ -47,6 +47,7 @@ import {
updateTextarea,
restoreControlledTextareaState,
} from './ReactDOMTextarea';
import {setSrcObject} from './ReactDOMSrcObject';
import {validateTextNesting} from './validateDOMNesting';
import {track} from './inputValueTracking';
import setTextContent from './setTextContent';
@@ -67,6 +68,7 @@ import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking
import {
enableScrollEndPolyfill,
enableSrcObject,
enableTrustedTypesIntegration,
} from 'shared/ReactFeatureFlags';
import {
@@ -402,7 +404,40 @@ function setProp(
break;
}
// fallthrough
case 'src':
case 'src': {
if (enableSrcObject && typeof value === 'object' && value !== null) {
// Some tags support object sources like Blob, File, MediaSource and MediaStream.
if (tag === 'img' || tag === 'video' || tag === 'audio') {
try {
setSrcObject(domElement, tag, value);
break;
} catch (x) {
// If URL.createObjectURL() errors, it was probably some other object type
// that should be toString:ed instead, so we just fall-through to the normal
// path.
}
} else {
if (__DEV__) {
try {
// This should always error.
URL.revokeObjectURL(URL.createObjectURL((value: any)));
if (tag === 'source') {
console.error(
'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
'Pass it directly to <img src>, <video src> or <audio src> instead.',
);
} else {
console.error(
'Passing Blob, MediaSource or MediaStream to <%s src> is not supported.',
tag,
);
}
} catch (x) {}
}
}
}
// Fallthrough
}
case 'href': {
if (
value === '' &&
@@ -2301,6 +2336,39 @@ function hydrateSanitizedAttribute(
warnForPropDifference(propKey, serverValue, value, serverDifferences);
}
function hydrateSrcObjectAttribute(
domElement: Element,
value: Blob,
extraAttributes: Set<string>,
serverDifferences: {[propName: string]: mixed},
): void {
const attributeName = 'src';
extraAttributes.delete(attributeName);
const serverValue = domElement.getAttribute(attributeName);
if (serverValue != null && value != null) {
const size = value.size;
const type = value.type;
if (typeof size === 'number' && typeof type === 'string') {
if (serverValue.indexOf('data:' + type + ';base64,') === 0) {
// For Blobs we don't bother reading the actual data but just diff by checking if
// the byte length size of the Blob maches the length of the data url.
const prefixLength = 5 + type.length + 8;
let byteLength = ((serverValue.length - prefixLength) / 4) * 3;
if (serverValue[serverValue.length - 1] === '=') {
byteLength--;
}
if (serverValue[serverValue.length - 2] === '=') {
byteLength--;
}
if (byteLength === size) {
return;
}
}
}
}
warnForPropDifference('src', serverValue, value, serverDifferences);
}
function diffHydratedCustomComponent(
domElement: Element,
tag: string,
@@ -2547,7 +2615,45 @@ function diffHydratedGenericElement(
continue;
}
// fallthrough
case 'src':
case 'src': {
if (enableSrcObject && typeof value === 'object' && value !== null) {
// Some tags support object sources like Blob, File, MediaSource and MediaStream.
if (tag === 'img' || tag === 'video' || tag === 'audio') {
try {
// Test if this is a compatible object
URL.revokeObjectURL(URL.createObjectURL((value: any)));
hydrateSrcObjectAttribute(
domElement,
value,
extraAttributes,
serverDifferences,
);
continue;
} catch (x) {
// If not, just fall through to the normal toString flow.
}
} else {
if (__DEV__) {
try {
// This should always error.
URL.revokeObjectURL(URL.createObjectURL((value: any)));
if (tag === 'source') {
console.error(
'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
'Pass it directly to <img src>, <video src> or <audio src> instead.',
);
} else {
console.error(
'Passing Blob, MediaSource or MediaStream to <%s src> is not supported.',
tag,
);
}
} catch (x) {}
}
}
}
// Fallthrough
}
case 'href':
if (
value === '' &&
@@ -211,7 +211,7 @@ export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
}
export function getFiberCurrentPropsFromNode(
node: Instance | TextInstance | SuspenseInstance,
node: Container | Instance | TextInstance | SuspenseInstance,
): Props {
return (node: any)[internalPropsKey] || null;
}
@@ -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.
*
* @flow
*/
export function setSrcObject(domElement: Element, tag: string, value: any) {
// We optimistically create the URL regardless of object type. This lets us
// support cross-realms and any type that the browser supports like new types.
const url = URL.createObjectURL((value: any));
const loadEvent = tag === 'img' ? 'load' : 'loadstart';
const cleanUp = () => {
// Once the object has started loading, then it's already collected by the
// browser and it won't refer to it by the URL anymore so we can now revoke it.
URL.revokeObjectURL(url);
domElement.removeEventListener(loadEvent, cleanUp);
domElement.removeEventListener('error', cleanUp);
};
domElement.addEventListener(loadEvent, cleanUp);
domElement.addEventListener('error', cleanUp);
domElement.setAttribute('src', url);
}
+349 -120
View File
@@ -25,11 +25,14 @@ import type {
PreinitScriptOptions,
PreinitModuleScriptOptions,
} from 'react-dom/src/shared/ReactDOMTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import type {TransitionTypes} from 'react/src/ReactTransitionType';
import {NotPending} from '../shared/ReactDOMFormActions';
import {setSrcObject} from './ReactDOMSrcObject';
import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
import hasOwnProperty from 'shared/hasOwnProperty';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
@@ -43,6 +46,8 @@ export {
import {
precacheFiberNode,
updateFiberProps,
getFiberCurrentPropsFromNode,
getInstanceFromNode,
getClosestInstanceFromNode,
getFiberFromScopeInstance,
getInstanceFromNode as getInstanceFromNodeDOMTree,
@@ -100,6 +105,9 @@ import {
disableLegacyMode,
enableMoveBefore,
disableCommentsAsDOMContainers,
enableSuspenseyImages,
enableSrcObject,
enableViewTransition,
} from 'shared/ReactFeatureFlags';
import {
HostComponent,
@@ -133,6 +141,11 @@ export type Props = {
'view-transition-name'?: string,
viewTransitionClass?: string,
'view-transition-class'?: string,
margin?: string,
marginTop?: string,
'margin-top'?: string,
marginBottom?: string,
'margin-bottom'?: string,
...
},
bottom?: null | number,
@@ -142,6 +155,10 @@ export type Props = {
is?: string,
size?: number,
multiple?: boolean,
src?: string | Blob | MediaSource | MediaStream, // TODO: Response
srcSet?: string,
loading?: 'eager' | 'lazy',
onLoad?: (event: any) => void,
...
};
type RawProps = {
@@ -213,9 +230,9 @@ const SUSPENSE_START_DATA = '$';
const SUSPENSE_END_DATA = '/$';
const SUSPENSE_PENDING_START_DATA = '$?';
const SUSPENSE_FALLBACK_START_DATA = '$!';
const PREAMBLE_CONTRIBUTION_HTML = 0b001;
const PREAMBLE_CONTRIBUTION_BODY = 0b010;
const PREAMBLE_CONTRIBUTION_HEAD = 0b100;
const PREAMBLE_CONTRIBUTION_HTML = 'html';
const PREAMBLE_CONTRIBUTION_BODY = 'body';
const PREAMBLE_CONTRIBUTION_HEAD = 'head';
const FORM_STATE_IS_MATCHING = 'F!';
const FORM_STATE_IS_NOT_MATCHING = 'F';
@@ -766,9 +783,25 @@ export function commitMount(
// only need to assign one. And Safari just never triggers a new load event which means this technique
// is already a noop regardless of which properties are assigned. We should revisit if browsers update
// this heuristic in the future.
if ((newProps: any).src) {
((domElement: any): HTMLImageElement).src = (newProps: any).src;
} else if ((newProps: any).srcSet) {
if (newProps.src) {
const src = (newProps: any).src;
if (enableSrcObject && typeof src === 'object') {
// For object src, we can't just set the src again to the same blob URL because it might have
// already revoked if it loaded before this. However, we can create a new blob URL and set that.
// This is relatively cheap since the blob is already in memory but this might cause some
// duplicated work.
// TODO: We could maybe detect if load hasn't fired yet and if so reuse the URL.
try {
setSrcObject(domElement, type, src);
return;
} catch (x) {
// If URL.createObjectURL() errors, it was probably some other object type
// that should be toString:ed instead, so we just fall-through to the normal
// path.
}
}
((domElement: any): HTMLImageElement).src = src;
} else if (newProps.srcSet) {
((domElement: any): HTMLImageElement).srcset = (newProps: any).srcSet;
}
return;
@@ -821,10 +854,51 @@ export function appendChild(
}
}
function warnForReactChildrenConflict(container: Container): void {
if (__DEV__) {
if ((container: any).__reactWarnedAboutChildrenConflict) {
return;
}
const props = getFiberCurrentPropsFromNode(container);
if (props !== null) {
const fiber = getInstanceFromNode(container);
if (fiber !== null) {
if (
typeof props.children === 'string' ||
typeof props.children === 'number'
) {
(container: any).__reactWarnedAboutChildrenConflict = true;
// Run the warning with the Fiber of the container for context of where the children are specified.
// We could also maybe use the Portal. The current execution context is the child being added.
runWithFiberInDEV(fiber, () => {
console.error(
'Cannot use a ref on a React element as a container to `createRoot` or `createPortal` ' +
'if that element also sets "children" text content using React. It should be a leaf with no children. ' +
"Otherwise it's ambiguous which children should be used.",
);
});
} else if (props.dangerouslySetInnerHTML != null) {
(container: any).__reactWarnedAboutChildrenConflict = true;
runWithFiberInDEV(fiber, () => {
console.error(
'Cannot use a ref on a React element as a container to `createRoot` or `createPortal` ' +
'if that element also sets "dangerouslySetInnerHTML" using React. It should be a leaf with no children. ' +
"Otherwise it's ambiguous which children should be used.",
);
});
}
}
}
}
}
export function appendChildToContainer(
container: Container,
child: Instance | TextInstance,
): void {
if (__DEV__) {
warnForReactChildrenConflict(container);
}
let parentNode: DocumentFragment | Element;
if (container.nodeType === DOCUMENT_NODE) {
parentNode = (container: any).body;
@@ -888,6 +962,9 @@ export function insertInContainerBefore(
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance,
): void {
if (__DEV__) {
warnForReactChildrenConflict(container);
}
let parentNode: DocumentFragment | Element;
if (container.nodeType === DOCUMENT_NODE) {
parentNode = (container: any).body;
@@ -977,7 +1054,6 @@ export function clearSuspenseBoundary(
suspenseInstance: SuspenseInstance,
): void {
let node: Node = suspenseInstance;
let possiblePreambleContribution: number = 0;
// Delete all nodes within this suspense boundary.
// There might be nested nodes so we need to keep track of how
// deep we are and only break out when we're back on top.
@@ -988,36 +1064,6 @@ export function clearSuspenseBoundary(
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
const data = ((nextNode: any).data: string);
if (data === SUSPENSE_END_DATA) {
if (
// represents 3 bits where at least one bit is set (1-7)
possiblePreambleContribution > 0 &&
possiblePreambleContribution < 8
) {
const code = possiblePreambleContribution;
// It's not normally possible to insert a comment immediately preceding Suspense boundary
// closing comment marker so we can infer that if the comment preceding starts with "1" through "7"
// then it is in fact a preamble contribution marker comment. We do this value test to avoid the case
// where the Suspense boundary is empty and the preceding comment marker is the Suspense boundary
// opening marker or the closing marker of an inner boundary. In those cases the first character won't
// have the requisite value to be interpreted as a Preamble contribution
const ownerDocument = parentInstance.ownerDocument;
if (code & PREAMBLE_CONTRIBUTION_HTML) {
const documentElement: Element =
(ownerDocument.documentElement: any);
releaseSingletonInstance(documentElement);
}
if (code & PREAMBLE_CONTRIBUTION_BODY) {
const body: Element = (ownerDocument.body: any);
releaseSingletonInstance(body);
}
if (code & PREAMBLE_CONTRIBUTION_HEAD) {
const head: Element = (ownerDocument.head: any);
releaseSingletonInstance(head);
// We need to clear the head because this is the only singleton that can have children that
// were part of this boundary but are not inside this boundary.
clearHead(head);
}
}
if (depth === 0) {
parentInstance.removeChild(nextNode);
// Retry if any event replaying was blocked on this.
@@ -1032,11 +1078,24 @@ export function clearSuspenseBoundary(
data === SUSPENSE_FALLBACK_START_DATA
) {
depth++;
} else {
possiblePreambleContribution = data.charCodeAt(0) - 48;
} else if (data === PREAMBLE_CONTRIBUTION_HTML) {
// If a preamble contribution marker is found within the bounds of this boundary,
// then it contributed to the html tag and we need to reset it.
const ownerDocument = parentInstance.ownerDocument;
const documentElement: Element = (ownerDocument.documentElement: any);
releaseSingletonInstance(documentElement);
} else if (data === PREAMBLE_CONTRIBUTION_HEAD) {
const ownerDocument = parentInstance.ownerDocument;
const head: Element = (ownerDocument.head: any);
releaseSingletonInstance(head);
// We need to clear the head because this is the only singleton that can have children that
// were part of this boundary but are not inside this boundary.
clearHead(head);
} else if (data === PREAMBLE_CONTRIBUTION_BODY) {
const ownerDocument = parentInstance.ownerDocument;
const body: Element = (ownerDocument.body: any);
releaseSingletonInstance(body);
}
} else {
possiblePreambleContribution = 0;
}
// $FlowFixMe[incompatible-type] we bail out when we get a null
node = nextNode;
@@ -1109,6 +1168,59 @@ export function unhideTextInstance(
textInstance.nodeValue = text;
}
function warnForBlockInsideInline(instance: HTMLElement) {
if (__DEV__) {
let nextNode = instance.firstChild;
outer: while (nextNode != null) {
let node: Node = nextNode;
if (
node.nodeType === ELEMENT_NODE &&
getComputedStyle((node: any)).display === 'block'
) {
console.error(
"You're about to start a <ViewTransition> around a display: inline " +
'element <%s>, which itself has a display: block element <%s> inside it. ' +
'This might trigger a bug in Safari which causes the View Transition to ' +
'be skipped with a duplicate name error.\n' +
'https://bugs.webkit.org/show_bug.cgi?id=290923',
instance.tagName.toLocaleLowerCase(),
(node: any).tagName.toLocaleLowerCase(),
);
break;
}
if (node.firstChild != null) {
nextNode = node.firstChild;
continue;
}
if (node === instance) {
break;
}
while (node.nextSibling == null) {
if (node.parentNode == null || node.parentNode === instance) {
break;
}
node = node.parentNode;
}
nextNode = node.nextSibling;
}
}
}
function countClientRects(rects: Array<ClientRect>): number {
if (rects.length === 1) {
return 1;
}
// Count non-zero rects.
let count = 0;
for (let i = 0; i < rects.length; i++) {
const rect = rects[i];
if (rect.width > 0 && rect.height > 0) {
count++;
}
}
return count;
}
export function applyViewTransitionName(
instance: Instance,
name: string,
@@ -1121,6 +1233,34 @@ export function applyViewTransitionName(
// $FlowFixMe[prop-missing]
instance.style.viewTransitionClass = className;
}
const computedStyle = getComputedStyle(instance);
if (computedStyle.display === 'inline') {
// WebKit has a bug where assigning a name to display: inline elements errors
// if they have display: block children. We try to work around this bug in the
// simple case by converting it automatically to display: inline-block.
// https://bugs.webkit.org/show_bug.cgi?id=290923
const rects = instance.getClientRects();
if (countClientRects(rects) === 1) {
// If the instance has a single client rect, that means that it can be
// expressed as a display: inline-block or block.
// This will cause layout thrash but we live with it since inline view transitions
// are unusual.
const style = instance.style;
// If there's literally only one rect, then it's likely on a single line like an
// inline-block. If it's multiple rects but all but one of them are empty it's
// likely because it's a single block that caused a line break.
style.display = rects.length === 1 ? 'inline-block' : 'block';
// Margin doesn't apply to inline so should be zero. However, padding top/bottom
// applies to inline-block positioning which we can offset by setting the margin
// to the negative padding to get it back into original position.
style.marginTop = '-' + computedStyle.paddingTop;
style.marginBottom = '-' + computedStyle.paddingBottom;
} else {
// This case cannot be easily fixed if it has blocks but it's also fine if
// it doesn't have blocks. So we only warn in DEV about this being an issue.
warnForBlockInsideInline(instance);
}
}
}
export function restoreViewTransitionName(
@@ -1128,6 +1268,7 @@ export function restoreViewTransitionName(
props: Props,
): void {
instance = ((instance: any): HTMLElement);
const style = instance.style;
const styleProp = props[STYLE];
const viewTransitionName =
styleProp != null
@@ -1138,7 +1279,7 @@ export function restoreViewTransitionName(
: null
: null;
// $FlowFixMe[prop-missing]
instance.style.viewTransitionName =
style.viewTransitionName =
viewTransitionName == null || typeof viewTransitionName === 'boolean'
? ''
: // The value would've errored already if it wasn't safe.
@@ -1153,12 +1294,39 @@ export function restoreViewTransitionName(
: null
: null;
// $FlowFixMe[prop-missing]
instance.style.viewTransitionClass =
style.viewTransitionClass =
viewTransitionClass == null || typeof viewTransitionClass === 'boolean'
? ''
: // The value would've errored already if it wasn't safe.
// eslint-disable-next-line react-internal/safe-string-coercion
('' + viewTransitionClass).trim();
if (style.display === 'inline-block') {
// We might have overridden the style. Reset it to what it should be.
if (styleProp == null) {
style.display = style.margin = '';
} else {
const display = styleProp.display;
style.display =
display == null || typeof display === 'boolean' ? '' : display;
const margin = styleProp.margin;
if (margin != null) {
style.margin = margin;
} else {
const marginTop = styleProp.hasOwnProperty('marginTop')
? styleProp.marginTop
: styleProp['margin-top'];
style.marginTop =
marginTop == null || typeof marginTop === 'boolean' ? '' : marginTop;
const marginBottom = styleProp.hasOwnProperty('marginBottom')
? styleProp.marginBottom
: styleProp['margin-bottom'];
style.marginBottom =
marginBottom == null || typeof marginBottom === 'boolean'
? ''
: marginBottom;
}
}
}
}
export function cancelViewTransitionName(
@@ -1358,7 +1526,9 @@ export function cloneRootViewTransitionContainer(
const containerParent = containerInstance.parentNode;
if (containerParent === null) {
throw new Error('Cannot use a useSwipeTransition() in a detached root.');
throw new Error(
'Cannot use a startGestureTransition() on a detached root.',
);
}
const clone: HTMLElement = containerInstance.cloneNode(false);
@@ -1464,7 +1634,9 @@ export function removeRootViewTransitionClone(
}
const containerParent = containerInstance.parentNode;
if (containerParent === null) {
throw new Error('Cannot use a useSwipeTransition() in a detached root.');
throw new Error(
'Cannot use a startGestureTransition() on a detached root.',
);
}
// We assume that the clone is still within the same parent.
containerParent.removeChild(clone);
@@ -1751,6 +1923,12 @@ export function startViewTransition(
}
} finally {
// Continue the reset of the work.
// If the error happened in the snapshot phase before the update callback
// was invoked, then we need to first finish the mutation and layout phases.
// If they're already invoked it's still safe to call them due the status check.
mutationCallback();
layoutCallback();
// Skip afterMutationCallback() since we're not animating.
spawnedWorkCallback();
}
};
@@ -1876,6 +2054,7 @@ function animateGesture(
// keyframe. Otherwise it applies to every keyframe.
moveOldFrameIntoViewport(keyframes[0]);
}
// TODO: Reverse the reverse if the original direction is reverse.
const reverse = rangeStart > rangeEnd;
targetElement.animate(keyframes, {
pseudoElement: pseudoElement,
@@ -1886,7 +2065,7 @@ function animateGesture(
// from scroll bouncing.
easing: 'linear',
// We fill in both direction for overscroll.
fill: 'both',
fill: 'both', // TODO: Should we preserve the fill instead?
// We play all gestures in reverse, except if we're in reverse direction
// in which case we need to play it in reverse of the reverse.
direction: reverse ? 'normal' : 'reverse',
@@ -1931,18 +2110,33 @@ export function startGestureTransition(
// up if they exist later.
const foundGroups: Set<string> = new Set();
const foundNews: Set<string> = new Set();
// Collect the longest duration of any view-transition animation including delay.
let longestDuration = 0;
for (let i = 0; i < animations.length; i++) {
const effect: KeyframeEffect = (animations[i].effect: any);
// $FlowFixMe
const pseudoElement: ?string = animations[i].effect.pseudoElement;
const pseudoElement: ?string = effect.pseudoElement;
if (pseudoElement == null) {
} else if (pseudoElement.startsWith('::view-transition-group')) {
foundGroups.add(pseudoElement.slice(23));
} else if (pseudoElement.startsWith('::view-transition-new')) {
// TODO: This is not really a sufficient detection because if the new
// pseudo element might exist but have animations disabled on it.
foundNews.add(pseudoElement.slice(21));
} else if (pseudoElement.startsWith('::view-transition')) {
const timing = effect.getTiming();
const duration =
typeof timing.duration === 'number' ? timing.duration : 0;
// TODO: Consider interation count higher than 1.
const durationWithDelay = timing.delay + duration;
if (durationWithDelay > longestDuration) {
longestDuration = durationWithDelay;
}
if (pseudoElement.startsWith('::view-transition-group')) {
foundGroups.add(pseudoElement.slice(23));
} else if (pseudoElement.startsWith('::view-transition-new')) {
// TODO: This is not really a sufficient detection because if the new
// pseudo element might exist but have animations disabled on it.
foundNews.add(pseudoElement.slice(21));
}
}
}
const durationToRangeMultipler =
(rangeEnd - rangeStart) / longestDuration;
for (let i = 0; i < animations.length; i++) {
const anim = animations[i];
if (anim.playState !== 'running') {
@@ -1982,14 +2176,33 @@ export function startGestureTransition(
}
// TODO: If this has only an old state and no new state,
}
// Adjust the range based on how long the animation would've ran as time based.
// Since we're running animations in reverse from how they normally would run,
// therefore the timing is from the rangeEnd to the start.
const timing = effect.getTiming();
const duration =
typeof timing.duration === 'number' ? timing.duration : 0;
let adjustedRangeStart =
rangeEnd - (duration + timing.delay) * durationToRangeMultipler;
let adjustedRangeEnd =
rangeEnd - timing.delay * durationToRangeMultipler;
if (
timing.direction === 'reverse' ||
timing.direction === 'alternate-reverse'
) {
// This animation was originally in reverse so we have to play it in flipped range.
const temp = adjustedRangeStart;
adjustedRangeStart = adjustedRangeEnd;
adjustedRangeEnd = temp;
}
animateGesture(
effect.getKeyframes(),
// $FlowFixMe: Always documentElement atm.
effect.target,
pseudoElement,
timeline,
rangeStart,
rangeEnd,
adjustedRangeStart,
adjustedRangeEnd,
isGeneratedGroupAnim,
isExitGroupAnim,
);
@@ -2051,7 +2264,13 @@ export function startGestureTransition(
}
} finally {
// Continue the reset of the work.
readyCallback();
// If the error happened in the snapshot phase before the update callback
// was invoked, then we need to first finish the mutation and layout phases.
// If they're already invoked it's still safe to call them due the status check.
mutationCallback();
// Skip readyCallback() and go straight to animateCallbck() since we're not animating.
// animateCallback() is still required to restore states.
animateCallback();
}
};
transition.ready.then(readyForAnimations, handleError);
@@ -2172,71 +2391,12 @@ export function getCurrentGestureOffset(provider: GestureTimeline): number {
return typeof time === 'number' ? time : time.value;
}
export function subscribeToGestureDirection(
provider: GestureTimeline,
currentOffset: number,
directionCallback: (direction: boolean) => void,
): () => void {
if (
typeof ScrollTimeline === 'function' &&
provider instanceof ScrollTimeline
) {
// For ScrollTimeline we optimize to only update the current time on scroll events.
const element = provider.source;
const scrollCallback = () => {
const newTime = provider.currentTime;
if (newTime !== null) {
const newValue = typeof newTime === 'number' ? newTime : newTime.value;
if (newValue !== currentOffset) {
directionCallback(newValue > currentOffset);
}
}
};
element.addEventListener('scroll', scrollCallback, false);
return () => {
element.removeEventListener('scroll', scrollCallback, false);
};
} else {
// For other AnimationTimelines, such as DocumentTimeline, we just update every rAF.
// TODO: Optimize ViewTimeline using an IntersectionObserver if it becomes common.
const rafCallback = () => {
const newTime = provider.currentTime;
if (newTime !== null) {
const newValue = typeof newTime === 'number' ? newTime : newTime.value;
if (newValue !== currentOffset) {
directionCallback(newValue > currentOffset);
}
}
callbackID = requestAnimationFrame(rafCallback);
};
let callbackID = requestAnimationFrame(rafCallback);
return () => {
cancelAnimationFrame(callbackID);
};
}
}
type EventListenerOptionsOrUseCapture =
| boolean
| {
capture?: boolean,
once?: boolean,
passive?: boolean,
signal?: AbortSignal,
...
};
type StoredEventListener = {
type: string,
listener: EventListener,
optionsOrUseCapture: void | EventListenerOptionsOrUseCapture,
};
type FocusOptions = {
preventScroll?: boolean,
focusVisible?: boolean,
};
export type FragmentInstanceType = {
_fragmentFiber: Fiber,
_eventListeners: null | Array<StoredEventListener>,
@@ -4935,6 +5095,36 @@ export function isHostHoistableType(
}
export function maySuspendCommit(type: Type, props: Props): boolean {
if (!enableSuspenseyImages && !enableViewTransition) {
return false;
}
// Suspensey images are the default, unless you opt-out of with either
// loading="lazy" or onLoad={...} which implies you're ok waiting.
return (
type === 'img' &&
props.src != null &&
props.src !== '' &&
props.onLoad == null &&
props.loading !== 'lazy'
);
}
export function maySuspendCommitOnUpdate(
type: Type,
oldProps: Props,
newProps: Props,
): boolean {
return (
maySuspendCommit(type, newProps) &&
(newProps.src !== oldProps.src || newProps.srcSet !== oldProps.srcSet)
);
}
export function maySuspendCommitInSyncRender(
type: Type,
props: Props,
): boolean {
// TODO: Allow sync lanes to suspend too with an opt-in.
return false;
}
@@ -4945,8 +5135,17 @@ export function mayResourceSuspendCommit(resource: Resource): boolean {
);
}
export function preloadInstance(type: Type, props: Props): boolean {
return true;
export function preloadInstance(
instance: Instance,
type: Type,
props: Props,
): boolean {
// We don't need to preload Suspensey images because the browser will
// load them early once we set the src.
// If we return true here, we'll still get a suspendInstance call in the
// pre-commit phase to determine if we still need to decode the image or
// if was dropped from cache. This just avoids rendering Suspense fallback.
return !!(instance: any).complete;
}
export function preloadResource(resource: Resource): boolean {
@@ -4983,8 +5182,38 @@ export function startSuspendingCommit(): void {
};
}
export function suspendInstance(type: Type, props: Props): void {
return;
const SUSPENSEY_IMAGE_TIMEOUT = 500;
export function suspendInstance(
instance: Instance,
type: Type,
props: Props,
): void {
if (!enableSuspenseyImages && !enableViewTransition) {
return;
}
if (suspendedState === null) {
throw new Error(
'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
);
}
const state = suspendedState;
if (
// $FlowFixMe[prop-missing]
typeof instance.decode === 'function' &&
typeof setTimeout === 'function'
) {
// If this browser supports decode() API, we use it to suspend waiting on the image.
// The loading should have already started at this point, so it should be enough to
// just call decode() which should also wait for the data to finish loading.
state.count++;
const ping = onUnsuspend.bind(state);
Promise.race([
// $FlowFixMe[prop-missing]
instance.decode(),
new Promise(resolve => setTimeout(resolve, SUSPENSEY_IMAGE_TIMEOUT)),
]).then(ping, ping);
}
}
export function suspendResource(
@@ -80,3 +80,14 @@ export function closeWithError(destination: Destination, error: mixed): void {
}
export {createFastHashJS as createFastHash} from 'react-server/src/createFastHashJS';
export function readAsDataURL(blob: Blob): Promise<string> {
return blob.arrayBuffer().then(arrayBuffer => {
const encoded =
typeof Buffer === 'function' && typeof Buffer.from === 'function'
? Buffer.from(arrayBuffer).toString('base64')
: btoa(String.fromCharCode.apply(String, new Uint8Array(arrayBuffer)));
const mimeType = blob.type || 'application/octet-stream';
return 'data:' + mimeType + ';base64,' + encoded;
});
}
+100 -38
View File
@@ -7,7 +7,11 @@
* @flow
*/
import type {ReactNodeList, ReactCustomFormAction} from 'shared/ReactTypes';
import type {
ReactNodeList,
ReactCustomFormAction,
Thenable,
} from 'shared/ReactTypes';
import type {
CrossOriginEnum,
PreloadImplOptions,
@@ -27,7 +31,10 @@ import {
import {Children} from 'react';
import {enableFizzExternalRuntime} from 'shared/ReactFeatureFlags';
import {
enableFizzExternalRuntime,
enableSrcObject,
} from 'shared/ReactFeatureFlags';
import type {
Destination,
@@ -42,6 +49,7 @@ import {
writeChunkAndReturn,
stringToChunk,
stringToPrecomputedChunk,
readAsDataURL,
} from 'react-server/src/ReactServerStreamConfig';
import {
resolveRequest,
@@ -684,23 +692,16 @@ export function completeResumableState(resumableState: ResumableState): void {
resumableState.bootstrapModules = undefined;
}
const NoContribution /* */ = 0b000;
const HTMLContribution /* */ = 0b001;
const BodyContribution /* */ = 0b010;
const HeadContribution /* */ = 0b100;
export type PreambleState = {
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
headChunks: null | Array<Chunk | PrecomputedChunk>,
bodyChunks: null | Array<Chunk | PrecomputedChunk>,
contribution: number,
};
export function createPreambleState(): PreambleState {
return {
htmlChunks: null,
headChunks: null,
bodyChunks: null,
contribution: NoContribution,
};
}
@@ -1214,6 +1215,47 @@ function pushFormActionAttribute(
return formData;
}
let blobCache: null | WeakMap<Blob, Thenable<string>> = null;
function pushSrcObjectAttribute(
target: Array<Chunk | PrecomputedChunk>,
blob: Blob,
): void {
// Throwing a Promise style suspense read of the Blob content.
if (blobCache === null) {
blobCache = new WeakMap();
}
const suspenseCache: WeakMap<Blob, Thenable<string>> = blobCache;
let thenable = suspenseCache.get(blob);
if (thenable === undefined) {
thenable = ((readAsDataURL(blob): any): Thenable<string>);
thenable.then(
result => {
(thenable: any).status = 'fulfilled';
(thenable: any).value = result;
},
error => {
(thenable: any).status = 'rejected';
(thenable: any).reason = error;
},
);
suspenseCache.set(blob, thenable);
}
if (thenable.status === 'rejected') {
throw thenable.reason;
} else if (thenable.status !== 'fulfilled') {
throw thenable;
}
const url = thenable.value;
target.push(
attributeSeparator,
stringToChunk('src'),
attributeAssign,
stringToChunk(escapeTextForBrowser(url)),
attributeEnd,
);
}
function pushAttribute(
target: Array<Chunk | PrecomputedChunk>,
name: string,
@@ -1243,7 +1285,15 @@ function pushAttribute(
pushStyleAttribute(target, value);
return;
}
case 'src':
case 'src': {
if (enableSrcObject && typeof value === 'object' && value !== null) {
if (typeof Blob === 'function' && value instanceof Blob) {
pushSrcObjectAttribute(target, value);
return;
}
}
// Fallthrough to general urls
}
case 'href': {
if (value === '') {
if (__DEV__) {
@@ -3222,6 +3272,12 @@ function pushTitleImpl(
return null;
}
// These are used by the client if we clear a boundary and we find these, then we
// also clear the singleton as well.
const headPreambleContributionChunk = stringToPrecomputedChunk('<!--head-->');
const bodyPreambleContributionChunk = stringToPrecomputedChunk('<!--body-->');
const htmlPreambleContributionChunk = stringToPrecomputedChunk('<!--html-->');
function pushStartHead(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
@@ -3236,6 +3292,12 @@ function pushStartHead(
if (preamble.headChunks) {
throw new Error(`The ${'`<head>`'} tag may only be rendered once.`);
}
// Insert a marker in the body where the contribution to the head was in case we need to clear it.
if (preambleState !== null) {
target.push(headPreambleContributionChunk);
}
preamble.headChunks = [];
return pushStartSingletonElement(preamble.headChunks, props, 'head');
} else {
@@ -3260,6 +3322,11 @@ function pushStartBody(
throw new Error(`The ${'`<body>`'} tag may only be rendered once.`);
}
// Insert a marker in the body where the contribution to the body tag was in case we need to clear it.
if (preambleState !== null) {
target.push(bodyPreambleContributionChunk);
}
preamble.bodyChunks = [];
return pushStartSingletonElement(preamble.bodyChunks, props, 'body');
} else {
@@ -3284,6 +3351,11 @@ function pushStartHtml(
throw new Error(`The ${'`<html>`'} tag may only be rendered once.`);
}
// Insert a marker in the body where the contribution to the head was in case we need to clear it.
if (preambleState !== null) {
target.push(htmlPreambleContributionChunk);
}
preamble.htmlChunks = [DOCTYPE];
return pushStartSingletonElement(preamble.htmlChunks, props, 'html');
} else {
@@ -3956,15 +4028,12 @@ export function hoistPreambleState(
const rootPreamble = renderState.preamble;
if (rootPreamble.htmlChunks === null && preambleState.htmlChunks) {
rootPreamble.htmlChunks = preambleState.htmlChunks;
preambleState.contribution |= HTMLContribution;
}
if (rootPreamble.headChunks === null && preambleState.headChunks) {
rootPreamble.headChunks = preambleState.headChunks;
preambleState.contribution |= HeadContribution;
}
if (rootPreamble.bodyChunks === null && preambleState.bodyChunks) {
rootPreamble.bodyChunks = preambleState.bodyChunks;
preambleState.contribution |= BodyContribution;
}
}
@@ -4030,6 +4099,24 @@ export function writePlaceholder(
return writeChunkAndReturn(destination, placeholder2);
}
// Activity boundaries are encoded as comments.
const startActivityBoundary = stringToPrecomputedChunk('<!--&-->');
const endActivityBoundary = stringToPrecomputedChunk('<!--/&-->');
export function pushStartActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
target.push(startActivityBoundary);
}
export function pushEndActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
target.push(endActivityBoundary);
}
// Suspense boundaries are encoded as comments.
const startCompletedSuspenseBoundary = stringToPrecomputedChunk('<!--$-->');
const startPendingSuspenseBoundary1 = stringToPrecomputedChunk(
@@ -4141,11 +4228,7 @@ export function writeStartClientRenderedSuspenseBoundary(
export function writeEndCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
preambleState: null | PreambleState,
): boolean {
if (preambleState) {
writePreambleContribution(destination, preambleState);
}
return writeChunkAndReturn(destination, endSuspenseBoundary);
}
export function writeEndPendingSuspenseBoundary(
@@ -4157,31 +4240,10 @@ export function writeEndPendingSuspenseBoundary(
export function writeEndClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
preambleState: null | PreambleState,
): boolean {
if (preambleState) {
writePreambleContribution(destination, preambleState);
}
return writeChunkAndReturn(destination, endSuspenseBoundary);
}
const boundaryPreambleContributionChunkStart = stringToPrecomputedChunk('<!--');
const boundaryPreambleContributionChunkEnd = stringToPrecomputedChunk('-->');
function writePreambleContribution(
destination: Destination,
preambleState: PreambleState,
) {
const contribution = preambleState.contribution;
if (contribution !== NoContribution) {
writeChunk(destination, boundaryPreambleContributionChunkStart);
// This is a number type so we can do the fast path without coercion checking
// eslint-disable-next-line react-internal/safe-string-coercion
writeChunk(destination, stringToChunk('' + contribution));
writeChunk(destination, boundaryPreambleContributionChunkEnd);
}
}
const startSegmentHTML = stringToPrecomputedChunk('<div hidden id="');
const startSegmentHTML2 = stringToPrecomputedChunk('">');
const endSegmentHTML = stringToPrecomputedChunk('</div>');
@@ -20,6 +20,8 @@ import {
createRenderState as createRenderStateImpl,
pushTextInstance as pushTextInstanceImpl,
pushSegmentFinale as pushSegmentFinaleImpl,
pushStartActivityBoundary as pushStartActivityBoundaryImpl,
pushEndActivityBoundary as pushEndActivityBoundaryImpl,
writeStartCompletedSuspenseBoundary as writeStartCompletedSuspenseBoundaryImpl,
writeStartClientRenderedSuspenseBoundary as writeStartClientRenderedSuspenseBoundaryImpl,
writeEndCompletedSuspenseBoundary as writeEndCompletedSuspenseBoundaryImpl,
@@ -207,6 +209,28 @@ export function pushSegmentFinale(
}
}
export function pushStartActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
if (renderState.generateStaticMarkup) {
// A completed boundary is done and doesn't need a representation in the HTML
// if we're not going to be hydrating it.
return;
}
pushStartActivityBoundaryImpl(target, renderState);
}
export function pushEndActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
if (renderState.generateStaticMarkup) {
return;
}
pushEndActivityBoundaryImpl(target, renderState);
}
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
@@ -244,30 +268,20 @@ export function writeStartClientRenderedSuspenseBoundary(
export function writeEndCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
preambleState: null | PreambleState,
): boolean {
if (renderState.generateStaticMarkup) {
return true;
}
return writeEndCompletedSuspenseBoundaryImpl(
destination,
renderState,
preambleState,
);
return writeEndCompletedSuspenseBoundaryImpl(destination, renderState);
}
export function writeEndClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
preambleState: null | PreambleState,
): boolean {
if (renderState.generateStaticMarkup) {
return true;
}
return writeEndClientRenderedSuspenseBoundaryImpl(
destination,
renderState,
preambleState,
);
return writeEndClientRenderedSuspenseBoundaryImpl(destination, renderState);
}
export type TransitionStatus = FormStatus;
+3 -3
View File
@@ -17,10 +17,10 @@
},
"homepage": "https://react.dev/",
"dependencies": {
"scheduler": "^0.25.0"
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.0.0"
"react": "^19.1.0"
},
"files": [
"LICENSE",
@@ -123,4 +123,4 @@
"./server.js": "./server.browser.js",
"./static.js": "./static.browser.js"
}
}
}

Some files were not shown because too many files have changed in this diff Show More