Compare commits

..

1 Commits

Author SHA1 Message Date
Lauren Tan
71c4b320aa [ci] Add ghstack /land bot
Adds a new `/land` command that can be written as a comment on a pull request. The command must be the very first line of the comment, like so:

```
/land

<additional context etc>
```

The workflow will first check if the commenter is a collaborator or member, and additionally also check if the commenter is a maintainer via the MAINTAINERS file.

The workflow will then attempt to validate the pull request, checking that CI has completed successfully and that it has received at least one approval before landing. The land is performed via `ghstack land`, which does mean that the PR itself isn't merged directly via github but it is pushed to main by a synthetic user (@facebook-github-bot for now). This means PRs landed with `/land` will have an additional co-author @facebook-github-bot, but the original committer will not be lost.
2025-04-24 15:50:58 -04:00
41 changed files with 655 additions and 764 deletions

View File

@@ -0,0 +1,221 @@
#!/usr/bin/env node
// JS rewrite of https://github.com/Chillee/ghstack_land_example/blob/main/.github/workflows/scripts/ghstack-perm-check.py
'use strict';
const {spawnSync} = require('child_process');
const process = require('process');
const {Octokit} = require('@octokit/rest');
const OWNER = 'facebook';
const REPO = 'react';
async function must(cond, msg, octokit, issue_number) {
if (!cond) {
console.error(msg);
try {
await octokit.issues.createComment({
owner: OWNER,
repo: REPO,
issue_number,
body: `ghstack bot failed: ${msg}`,
});
} catch (error) {
console.error('Failed to post comment:', error);
}
process.exit(1);
}
}
async function main() {
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
if (!GITHUB_TOKEN) {
console.error('GITHUB_TOKEN environment variable is not set.');
process.exit(1);
}
const octokit = new Octokit({auth: GITHUB_TOKEN});
const prNumber = parseInt(process.argv[2]);
const headRef = process.argv[3];
console.log(headRef);
await must(
headRef && /^gh\/[A-Za-z0-9-]+\/[0-9]+\/head$/.test(headRef),
'Not a ghstack PR',
octokit,
OWNER,
REPO,
prNumber
);
const origRef = headRef.replace('/head', '/orig');
console.log(':: Fetching newest main...');
let result = spawnSync('git', ['fetch', 'origin', 'main'], {
stdio: 'inherit',
});
await must(
result.status === 0,
"Can't fetch main",
octokit,
OWNER,
REPO,
prNumber
);
console.log(':: Fetching orig branch...');
result = spawnSync('git', ['fetch', 'origin', origRef], {stdio: 'inherit'});
await must(
result.status === 0,
"Can't fetch orig branch",
octokit,
OWNER,
REPO,
prNumber
);
result = spawnSync(
'git',
['log', 'FETCH_HEAD...$(git merge-base FETCH_HEAD origin/main)'],
{shell: true}
);
const out = result.stdout.toString();
await must(
result.status === 0,
'`git log` command failed!',
octokit,
OWNER,
REPO,
prNumber
);
const regex =
/Pull Request resolved: https:\/\/github\.com\/.*?\/pull\/([0-9]+)/g;
const prNumbers = [];
let match;
while ((match = regex.exec(out)) !== null) {
prNumbers.push(parseInt(match[1], 10));
}
console.log(prNumbers);
await must(
prNumbers.length && prNumbers[0] === prNumber,
'Extracted PR numbers not seems right!',
octokit,
OWNER,
REPO,
prNumber
);
for (const n of prNumbers) {
process.stdout.write(`:: Checking PR status #${n}... `);
let prObj;
try {
const {data} = await octokit.pulls.get({
owner: OWNER,
repo: REPO,
pull_number: n,
});
prObj = data;
} catch (error) {
await must(
false,
'Error Getting PR Object!',
octokit,
OWNER,
REPO,
prNumber
);
}
let reviews;
try {
const {data} = await octokit.request(
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
{
owner: OWNER,
repo: REPO,
pull_number: prNumber,
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
reviews = data;
} catch (error) {
await must(
false,
'Error Getting PR Reviews!',
octokit,
OWNER,
REPO,
prNumber
);
}
let approved = false;
for (const review of reviews) {
if (review.state === 'COMMENTED') continue;
await must(
['APPROVED', 'DISMISSED'].includes(review.state),
`@${review.user.login} has stamped PR #${n} \`${review.state}\`, please resolve it first!`,
octokit,
OWNER,
REPO,
prNumber
);
if (review.state === 'APPROVED') {
approved = true;
}
}
await must(
approved,
`PR #${n} is not approved yet!`,
octokit,
OWNER,
REPO,
prNumber
);
let checkruns;
try {
const {data} = await octokit.checks.listForRef({
owner: OWNER,
repo: REPO,
ref: prObj.head.sha,
});
checkruns = data;
} catch (error) {
await must(
false,
'Error getting check runs status!',
octokit,
OWNER,
REPO,
prNumber
);
}
for (const cr of checkruns.check_runs) {
const status = cr.conclusion ? cr.conclusion : cr.status;
const name = cr.name;
if (name === 'Copilot for PRs') continue;
await must(
['success', 'neutral'].includes(status),
`PR #${n} check-run \`${name}\`'s status \`${status}\` is not success!`,
octokit,
OWNER,
REPO,
prNumber
);
}
console.log('SUCCESS!');
}
console.log(':: All PRs are ready to be landed!');
}
main().catch(err => {
console.error('Unexpected error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,12 @@
{
"name": "ghstack-perm-check",
"version": "0.0.0",
"private": true,
"scripts": {
"check-permissions": "node ./check_permissions.js"
},
"license": "MIT",
"dependencies": {
"@octokit/rest": "^21.1.1"
}
}

View File

@@ -0,0 +1,112 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@octokit/auth-token@^5.0.0":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.2.tgz#68a486714d7a7fd1df56cb9bc89a860a0de866de"
integrity sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==
"@octokit/core@^6.1.4":
version "6.1.4"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-6.1.4.tgz#f5ccf911cc95b1ce9daf6de425d1664392f867db"
integrity sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==
dependencies:
"@octokit/auth-token" "^5.0.0"
"@octokit/graphql" "^8.1.2"
"@octokit/request" "^9.2.1"
"@octokit/request-error" "^6.1.7"
"@octokit/types" "^13.6.2"
before-after-hook "^3.0.2"
universal-user-agent "^7.0.0"
"@octokit/endpoint@^10.1.3":
version "10.1.3"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-10.1.3.tgz#bfe8ff2ec213eb4216065e77654bfbba0fc6d4de"
integrity sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==
dependencies:
"@octokit/types" "^13.6.2"
universal-user-agent "^7.0.2"
"@octokit/graphql@^8.1.2":
version "8.2.1"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-8.2.1.tgz#0cb83600e6b4009805acc1c56ae8e07e6c991b78"
integrity sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==
dependencies:
"@octokit/request" "^9.2.2"
"@octokit/types" "^13.8.0"
universal-user-agent "^7.0.0"
"@octokit/openapi-types@^24.2.0":
version "24.2.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-24.2.0.tgz#3d55c32eac0d38da1a7083a9c3b0cca77924f7d3"
integrity sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==
"@octokit/plugin-paginate-rest@^11.4.2":
version "11.6.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz#e5e9ff3530e867c3837fdbff94ce15a2468a1f37"
integrity sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==
dependencies:
"@octokit/types" "^13.10.0"
"@octokit/plugin-request-log@^5.3.1":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz#ccb75d9705de769b2aa82bcd105cc96eb0c00f69"
integrity sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==
"@octokit/plugin-rest-endpoint-methods@^13.3.0":
version "13.5.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz#d8c8ca2123b305596c959a9134dfa8b0495b0ba6"
integrity sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==
dependencies:
"@octokit/types" "^13.10.0"
"@octokit/request-error@^6.1.7":
version "6.1.7"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-6.1.7.tgz#44fc598f5cdf4593e0e58b5155fe2e77230ff6da"
integrity sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==
dependencies:
"@octokit/types" "^13.6.2"
"@octokit/request@^9.2.1", "@octokit/request@^9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-9.2.2.tgz#754452ec4692d7fdc32438a14e028eba0e6b2c09"
integrity sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==
dependencies:
"@octokit/endpoint" "^10.1.3"
"@octokit/request-error" "^6.1.7"
"@octokit/types" "^13.6.2"
fast-content-type-parse "^2.0.0"
universal-user-agent "^7.0.2"
"@octokit/rest@^21.1.1":
version "21.1.1"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-21.1.1.tgz#7a70455ca451b1d253e5b706f35178ceefb74de2"
integrity sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==
dependencies:
"@octokit/core" "^6.1.4"
"@octokit/plugin-paginate-rest" "^11.4.2"
"@octokit/plugin-request-log" "^5.3.1"
"@octokit/plugin-rest-endpoint-methods" "^13.3.0"
"@octokit/types@^13.10.0", "@octokit/types@^13.6.2", "@octokit/types@^13.8.0":
version "13.10.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.10.0.tgz#3e7c6b19c0236c270656e4ea666148c2b51fd1a3"
integrity sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==
dependencies:
"@octokit/openapi-types" "^24.2.0"
before-after-hook@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d"
integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==
fast-content-type-parse@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz#c236124534ee2cb427c8d8e5ba35a4856947847b"
integrity sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==
universal-user-agent@^7.0.0, universal-user-agent@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e"
integrity sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==

View File

@@ -0,0 +1,121 @@
name: (Shared) ghstack land
on:
issue_comment:
types: [created]
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
jobs:
check_access:
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_access.outputs.result }}
steps:
- name: Check access
id: check_access
if: ${{ github.event.comment.author_association == 'MEMBER' || github.event.comment.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]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
contents: read
with:
actor: ${{ github.event.comment.user.login }}
ghstack_land:
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/land') }}
needs: [check_maintainer]
runs-on: ubuntu-latest
steps:
- name: Add reaction to comment
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const comment_id = "${{ github.event.comment.id }}"
await github.rest.reactions.createForCommitComment({
owner,
repo,
comment_id,
content: "rocket",
});
- name: Get PR details
id: get-pr
run: |
PR_NUMBER=${{ github.event.issue.number }}
echo "PR number is $PR_NUMBER"
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
# Get PR details using GitHub API
PR_DATA=$(curl -s \
-H "Authorization: token ${{ github.token }}" \
-H "Accept: application/vnd.github.v3+json" \
"${{ github.api_url }}/repos/${{ github.repository }}/pulls/$PR_NUMBER")
# Extract useful information
PR_HEAD_REF=$(echo "$PR_DATA" | jq -r .head.ref)
PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r .head.sha)
PR_URL="${{ github.server_url }}/${{ github.repository }}/pull/$PR_NUMBER"
echo "pr_branch=$PR_HEAD_REF" >> $GITHUB_OUTPUT
echo "pr_sha=$PR_HEAD_SHA" >> $GITHUB_OUTPUT
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
echo "pr_branch=$PR_HEAD_REF"
echo "pr_sha=$PR_HEAD_SHA"
echo "pr_url=$PR_URL"
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ghstack-pip-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/requirements.txt') }}
- uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install ghstack
run: pip install requests ghstack
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: ghstack-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('.github/workflows/scripts/ghstack/yarn.lock') }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: .github/workflows/scripts/ghstack/yarn.lock
- run: yarn install --cwd .github/workflows/scripts/ghstack --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Check Current CI Status
run: |
echo ${{ github.event.issue.number }}
yarn --cwd .github/workflows/scripts/ghstack check-permissions ${{ github.event.issue.number }} ${{steps.get-pr.outputs.pr_branch}}
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Land It!
run: |
git config --global user.email "facebook-github-bot@users.noreply.github.com"
git config --global user.name "Facebook Community Bot"
cat <<EOF > ~/.ghstackrc
[ghstack]
github_url = github.com
github_oauth = $GITHUB_TOKEN
github_username = facebook-github-bot
remote_name = origin
EOF
ghstack land "${{ steps.get-pr.outputs.pr_url }}"
env:
GITHUB_TOKEN: ${{ github.token }}

View File

@@ -1,59 +0,0 @@
## 19.1.0-rc.1 (April 21, 2025)
## eslint-plugin-react-hooks
* Temporarily disable ref access in render validation [#32839](https://github.com/facebook/react/pull/32839) by [@poteto](https://github.com/poteto)
* Fix type error with recommended config [#32666](https://github.com/facebook/react/pull/32666) by [@niklasholm](https://github.com/niklasholm)
* Merge rule from eslint-plugin-react-compiler into `react-hooks` plugin [#32416](https://github.com/facebook/react/pull/32416) by [@michaelfaith](https://github.com/michaelfaith)
* Add dev dependencies for typescript migration [#32279](https://github.com/facebook/react/pull/32279) by [@michaelfaith](https://github.com/michaelfaith)
* Support v9 context api [#32045](https://github.com/facebook/react/pull/32045) by [@michaelfaith](https://github.com/michaelfaith)
* Support eslint 8+ flat plugin syntax out of the box for eslint-plugin-react-compiler [#32120](https://github.com/facebook/react/pull/32120) by [@orta](https://github.com/orta)
## babel-plugin-react-compiler
* Support satisfies operator [#32742](https://github.com/facebook/react/pull/32742) by [@rodrigofariow](https://github.com/rodrigofariow)
* Fix inferEffectDependencies lint false positives [#32769](https://github.com/facebook/react/pull/32769) by [@mofeiZ](https://github.com/mofeiZ)
* Fix hoisting of let declarations [#32724](https://github.com/facebook/react/pull/32724) by [@mofeiZ](https://github.com/mofeiZ)
* Avoid failing builds when import specifiers conflict or shadow vars [#32663](https://github.com/facebook/react/pull/32663) by [@mofeiZ](https://github.com/mofeiZ)
* Optimize components declared with arrow function and implicit return and `compilationMode: 'infer'` [#31792](https://github.com/facebook/react/pull/31792) by [@dimaMachina](https://github.com/dimaMachina)
* Validate static components [#32683](https://github.com/facebook/react/pull/32683) by [@josephsavona](https://github.com/josephsavona)
* Hoist dependencies from functions more conservatively [#32616](https://github.com/facebook/react/pull/32616) by [@mofeiZ](https://github.com/mofeiZ)
* Implement NumericLiteral as ObjectPropertyKey [#31791](https://github.com/facebook/react/pull/31791) by [@dimaMachina](https://github.com/dimaMachina)
* Avoid bailouts when inserting gating [#32598](https://github.com/facebook/react/pull/32598) by [@mofeiZ](https://github.com/mofeiZ)
* Stop bailing out early for hoisted gated functions [#32597](https://github.com/facebook/react/pull/32597) by [@mofeiZ](https://github.com/mofeiZ)
* Add shape for Array.from [#32522](https://github.com/facebook/react/pull/32522) by [@mofeiZ](https://github.com/mofeiZ)
* Patch array and argument spread mutability [#32521](https://github.com/facebook/react/pull/32521) by [@mofeiZ](https://github.com/mofeiZ)
* Make CompilerError compatible with reflection [#32539](https://github.com/facebook/react/pull/32539) by [@poteto](https://github.com/poteto)
* Add simple walltime measurement [#32331](https://github.com/facebook/react/pull/32331) by [@poteto](https://github.com/poteto)
* Improve error messages for unhandled terminal and instruction kinds [#32324](https://github.com/facebook/react/pull/32324) by [@inottn](https://github.com/inottn)
* Handle TSInstantiationExpression in lowerExpression [#32302](https://github.com/facebook/react/pull/32302) by [@inottn](https://github.com/inottn)
* Fix invalid Array.map type [#32095](https://github.com/facebook/react/pull/32095) by [@mofeiZ](https://github.com/mofeiZ)
* Patch for JSX escape sequences in @babel/generator [#32131](https://github.com/facebook/react/pull/32131) by [@mofeiZ](https://github.com/mofeiZ)
* `JSXText` emits incorrect with bracket [#32138](https://github.com/facebook/react/pull/32138) by [@himself65](https://github.com/himself65)
* Validation against calling impure functions [#31960](https://github.com/facebook/react/pull/31960) by [@josephsavona](https://github.com/josephsavona)
* Always target node [#32091](https://github.com/facebook/react/pull/32091) by [@poteto](https://github.com/poteto)
* Patch compilationMode:infer object method edge case [#32055](https://github.com/facebook/react/pull/32055) by [@mofeiZ](https://github.com/mofeiZ)
* Generate ts defs [#31994](https://github.com/facebook/react/pull/31994) by [@poteto](https://github.com/poteto)
* Relax react peer dep requirement [#31915](https://github.com/facebook/react/pull/31915) by [@poteto](https://github.com/poteto)
* Allow type cast expressions with refs [#31871](https://github.com/facebook/react/pull/31871) by [@josephsavona](https://github.com/josephsavona)
* Add shape for global Object.keys [#31583](https://github.com/facebook/react/pull/31583) by [@mofeiZ](https://github.com/mofeiZ)
* Optimize method calls w props receiver [#31775](https://github.com/facebook/react/pull/31775) by [@josephsavona](https://github.com/josephsavona)
* Fix dropped ref with spread props in InlineJsxTransform [#31726](https://github.com/facebook/react/pull/31726) by [@jackpope](https://github.com/jackpope)
* Support for non-declatation for in/of iterators [#31710](https://github.com/facebook/react/pull/31710) by [@mvitousek](https://github.com/mvitousek)
* Support for context variable loop iterators [#31709](https://github.com/facebook/react/pull/31709) by [@mvitousek](https://github.com/mvitousek)
* Replace deprecated dependency in `eslint-plugin-react-compiler` [#31629](https://github.com/facebook/react/pull/31629) by [@rakleed](https://github.com/rakleed)
* Support enableRefAsProp in jsx transform [#31558](https://github.com/facebook/react/pull/31558) by [@jackpope](https://github.com/jackpope)
* Fix: ref.current now correctly reactive [#31521](https://github.com/facebook/react/pull/31521) by [@mofeiZ](https://github.com/mofeiZ)
* Outline JSX with non-jsx children [#31442](https://github.com/facebook/react/pull/31442) by [@gsathya](https://github.com/gsathya)
* Outline jsx with duplicate attributes [#31441](https://github.com/facebook/react/pull/31441) by [@gsathya](https://github.com/gsathya)
* Store original and new prop names [#31440](https://github.com/facebook/react/pull/31440) by [@gsathya](https://github.com/gsathya)
* Stabilize compiler output: sort deps and decls by name [#31362](https://github.com/facebook/react/pull/31362) by [@mofeiZ](https://github.com/mofeiZ)
* Bugfix for hoistable deps for nested functions [#31345](https://github.com/facebook/react/pull/31345) by [@mofeiZ](https://github.com/mofeiZ)
* Remove compiler runtime-compat fixture library [#31430](https://github.com/facebook/react/pull/31430) by [@poteto](https://github.com/poteto)
* Wrap inline jsx transform codegen in conditional [#31267](https://github.com/facebook/react/pull/31267) by [@jackpope](https://github.com/jackpope)
* Check if local identifier is a hook when resolving globals [#31384](https://github.com/facebook/react/pull/31384) by [@poteto](https://github.com/poteto)
* Handle member expr as computed property [#31344](https://github.com/facebook/react/pull/31344) by [@gsathya](https://github.com/gsathya)
* Fix to ref access check to ban ref?.current [#31360](https://github.com/facebook/react/pull/31360) by [@mvitousek](https://github.com/mvitousek)
* InlineJSXTransform transforms jsx inside function expressions [#31282](https://github.com/facebook/react/pull/31282) by [@josephsavona](https://github.com/josephsavona)
## Other
* Add shebang to banner [#32225](https://github.com/facebook/react/pull/32225) by [@Jeremy-Hibiki](https://github.com/Jeremy-Hibiki)
* remove terser from react-compiler-runtime build [#31326](https://github.com/facebook/react/pull/31326) by [@henryqdineen](https://github.com/henryqdineen)

View File

@@ -19,7 +19,6 @@ import {
BasicBlock,
BlockId,
DependencyPathEntry,
FunctionExpression,
GeneratedSource,
getHookKind,
HIRFunction,
@@ -31,7 +30,6 @@ import {
PropertyLiteral,
ReactiveScopeDependency,
ScopeId,
TInstruction,
} from './HIR';
const DEBUG_PRINT = false;
@@ -129,33 +127,6 @@ 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>;

View File

@@ -116,7 +116,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
}
}
export function findTemporariesUsedOutsideDeclaringScope(
function findTemporariesUsedOutsideDeclaringScope(
fn: HIRFunction,
): ReadonlySet<DeclarationId> {
/*
@@ -378,7 +378,7 @@ type Decl = {
scope: Stack<ReactiveScope>;
};
export class DependencyCollectionContext {
class Context {
#declarations: Map<DeclarationId, Decl> = new Map();
#reassignments: Map<Identifier, Decl> = new Map();
@@ -645,10 +645,7 @@ enum HIRValue {
Terminal,
}
export function handleInstruction(
instr: Instruction,
context: DependencyCollectionContext,
): void {
function handleInstruction(instr: Instruction, context: Context): void {
const {id, value, lvalue} = instr;
context.declare(lvalue.identifier, {
id,
@@ -711,7 +708,7 @@ function collectDependencies(
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
const context = new DependencyCollectionContext(
const context = new Context(
usedOutsideDeclaringScope,
temporaries,
processedInstrsInOptional,

View File

@@ -22,30 +22,18 @@ 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';
/**
@@ -74,7 +62,10 @@ 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, ReactiveScopeDependencies>();
const scopeInfos = new Map<
ScopeId,
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
>();
const loadGlobals = new Set<IdentifierId>();
@@ -88,18 +79,19 @@ export function inferEffectDependencies(fn: HIRFunction): void {
const reactiveIds = inferReactiveIdentifiers(fn);
for (const [, block] of fn.body.blocks) {
if (block.terminal.kind === 'scope') {
if (
block.terminal.kind === 'scope' ||
block.terminal.kind === 'pruned-scope'
) {
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
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,
);
}
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,
});
}
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
for (const instr of block.instructions) {
@@ -181,12 +173,22 @@ export function inferEffectDependencies(fn: HIRFunction): void {
fnExpr.lvalue.identifier.scope != null
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
: null;
let minimalDeps: Set<ReactiveScopeDependency>;
if (scopeInfo != null) {
minimalDeps = new Set(scopeInfo);
} else {
minimalDeps = inferMinimalDependencies(fnExpr);
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,
});
}
/**
* Step 1: push dependencies to the effect deps array
*
@@ -194,9 +196,8 @@ export function inferEffectDependencies(fn: HIRFunction): void {
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
* explanation.
*/
const usedDeps = [];
for (const dep of minimalDeps) {
for (const dep of scopeInfo.deps) {
if (
((isUseRefType(dep.identifier) ||
isSetStateType(dep.identifier)) &&
@@ -421,132 +422,3 @@ function collectDepUsages(
return sourceLocations;
}
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);
}
}
}
}

View File

@@ -1,39 +0,0 @@
## 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

View File

@@ -1,12 +0,0 @@
// @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;
}

View File

@@ -1,38 +0,0 @@
## 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));
arrRef.current.val = 2;
return arrRef;
}
```
## 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]);
arrRef.current.val = 2;
return arrRef;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,11 +0,0 @@
// @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));
arrRef.current.val = 2;
return arrRef;
}

View File

@@ -1,34 +0,0 @@
## 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

View File

@@ -1,9 +0,0 @@
// @inferEffectDependencies @panicThreshold(none)
import {useEffect} from 'react';
function Component({foo}) {
const arr = [];
useEffect(() => arr.push(foo));
arr.push(2);
return arr;
}

View File

@@ -0,0 +1,42 @@
## 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 |
```

View File

@@ -1,54 +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;
}
```
## 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

View File

@@ -1,6 +1,5 @@
import React from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import {Writable} from 'stream';
import App from '../src/components/App';
@@ -15,41 +14,11 @@ if (process.env.NODE_ENV === 'development') {
assets = require('../build/asset-manifest.json');
}
class ThrottledWritable extends Writable {
constructor(destination) {
super();
this.destination = destination;
this.delay = 150;
}
_write(chunk, encoding, callback) {
let o = 0;
const write = () => {
this.destination.write(chunk.slice(o, o + 100), encoding, x => {
o += 100;
if (o < chunk.length) {
setTimeout(write, this.delay);
} else {
callback(x);
}
});
};
setTimeout(write, this.delay);
}
_final(callback) {
setTimeout(() => {
this.destination.end(callback);
}, this.delay);
}
}
export default function render(url, res) {
res.socket.on('error', error => {
// Log fatal errors
console.error('Fatal', error);
});
console.log('hello');
let didError = false;
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
bootstrapScripts: [assets['main.js']],
@@ -57,10 +26,7 @@ export default function render(url, res) {
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
// To test the actual chunks taking time to load over the network, we throttle
// the stream a bit.
const throttledResponse = new ThrottledWritable(res);
pipe(throttledResponse);
pipe(res);
},
onShellError(x) {
// Something errored before we could complete the shell so we emit an alternative shell.

View File

@@ -37,7 +37,6 @@ export default class Chrome extends Component {
</div>
</Theme.Provider>
</Suspense>
<p>This should appear in the first paint.</p>
<script
dangerouslySetInnerHTML={{
__html: `assetManifest = ${JSON.stringify(assets)};`,

View File

@@ -120,13 +120,12 @@ const ScriptStreamingFormat: StreamingFormat = 0;
const DataStreamingFormat: StreamingFormat = 1;
export type InstructionState = number;
const NothingSent /* */ = 0b000000;
const SentCompleteSegmentFunction /* */ = 0b000001;
const SentCompleteBoundaryFunction /* */ = 0b000010;
const SentClientRenderFunction /* */ = 0b000100;
const SentStyleInsertionFunction /* */ = 0b001000;
const SentFormReplayingRuntime /* */ = 0b010000;
const SentCompletedShellId /* */ = 0b100000;
const NothingSent /* */ = 0b00000;
const SentCompleteSegmentFunction /* */ = 0b00001;
const SentCompleteBoundaryFunction /* */ = 0b00010;
const SentClientRenderFunction /* */ = 0b00100;
const SentStyleInsertionFunction /* */ = 0b01000;
const SentFormReplayingRuntime /* */ = 0b10000;
// Per request, global state that is not contextual to the rendering subtree.
// This cannot be resumed and therefore should only contain things that are
@@ -290,15 +289,15 @@ export type ResumableState = {
const dataElementQuotedEnd = stringToPrecomputedChunk('"></template>');
const startInlineScript = stringToPrecomputedChunk('<script');
const startInlineScript = stringToPrecomputedChunk('<script>');
const endInlineScript = stringToPrecomputedChunk('</script>');
const startScriptSrc = stringToPrecomputedChunk('<script src="');
const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
const scriptNonce = stringToPrecomputedChunk(' nonce="');
const scriptIntegirty = stringToPrecomputedChunk(' integrity="');
const scriptCrossOrigin = stringToPrecomputedChunk(' crossorigin="');
const endAsyncScript = stringToPrecomputedChunk(' async=""></script>');
const scriptNonce = stringToPrecomputedChunk('" nonce="');
const scriptIntegirty = stringToPrecomputedChunk('" integrity="');
const scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
const endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
/**
* This escaping function is designed to work with with inline scripts where the entire
@@ -368,7 +367,7 @@ export function createRenderState(
nonce === undefined
? startInlineScript
: stringToPrecomputedChunk(
'<script nonce="' + escapeTextForBrowser(nonce) + '"',
'<script nonce="' + escapeTextForBrowser(nonce) + '">',
);
const idPrefix = resumableState.idPrefix;
@@ -377,10 +376,8 @@ export function createRenderState(
const {bootstrapScriptContent, bootstrapScripts, bootstrapModules} =
resumableState;
if (bootstrapScriptContent !== undefined) {
bootstrapChunks.push(inlineScriptWithNonce);
pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
bootstrapChunks.push(
endOfStartTag,
inlineScriptWithNonce,
stringToChunk(escapeEntireInlineScriptContent(bootstrapScriptContent)),
endInlineScript,
);
@@ -530,30 +527,25 @@ export function createRenderState(
bootstrapChunks.push(
startScriptSrc,
stringToChunk(escapeTextForBrowser(src)),
attributeEnd,
);
if (nonce) {
bootstrapChunks.push(
scriptNonce,
stringToChunk(escapeTextForBrowser(nonce)),
attributeEnd,
);
}
if (typeof integrity === 'string') {
bootstrapChunks.push(
scriptIntegirty,
stringToChunk(escapeTextForBrowser(integrity)),
attributeEnd,
);
}
if (typeof crossOrigin === 'string') {
bootstrapChunks.push(
scriptCrossOrigin,
stringToChunk(escapeTextForBrowser(crossOrigin)),
attributeEnd,
);
}
pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
bootstrapChunks.push(endAsyncScript);
}
}
@@ -587,30 +579,26 @@ export function createRenderState(
bootstrapChunks.push(
startModuleSrc,
stringToChunk(escapeTextForBrowser(src)),
attributeEnd,
);
if (nonce) {
bootstrapChunks.push(
scriptNonce,
stringToChunk(escapeTextForBrowser(nonce)),
attributeEnd,
);
}
if (typeof integrity === 'string') {
bootstrapChunks.push(
scriptIntegirty,
stringToChunk(escapeTextForBrowser(integrity)),
attributeEnd,
);
}
if (typeof crossOrigin === 'string') {
bootstrapChunks.push(
scriptCrossOrigin,
stringToChunk(escapeTextForBrowser(crossOrigin)),
attributeEnd,
);
}
pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
bootstrapChunks.push(endAsyncScript);
}
}
@@ -1972,32 +1960,11 @@ function injectFormReplayingRuntime(
(!enableFizzExternalRuntime || !renderState.externalRuntimeScript)
) {
resumableState.instructions |= SentFormReplayingRuntime;
const preamble = renderState.preamble;
const bootstrapChunks = renderState.bootstrapChunks;
if (
(preamble.htmlChunks || preamble.headChunks) &&
bootstrapChunks.length === 0
) {
// If we rendered the whole document, then we emitted a rel="expect" that needs a
// matching target. If we haven't emitted that yet, we need to include it in this
// script tag.
bootstrapChunks.push(renderState.startInlineScript);
pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
bootstrapChunks.push(
endOfStartTag,
formReplayingRuntimeScript,
endInlineScript,
);
} else {
// Otherwise we added to the beginning of the scripts. This will mean that it
// appears before the shell ID unfortunately.
bootstrapChunks.unshift(
renderState.startInlineScript,
endOfStartTag,
formReplayingRuntimeScript,
endInlineScript,
);
}
renderState.bootstrapChunks.unshift(
renderState.startInlineScript,
formReplayingRuntimeScript,
endInlineScript,
);
}
}
@@ -4108,21 +4075,8 @@ function writeBootstrap(
export function writeCompletedRoot(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
): boolean {
const preamble = renderState.preamble;
if (preamble.htmlChunks || preamble.headChunks) {
// If we rendered the whole document, then we emitted a rel="expect" that needs a
// matching target. Normally we use one of the bootstrap scripts for this but if
// there are none, then we need to emit a tag to complete the shell.
if ((resumableState.instructions & SentCompletedShellId) === NothingSent) {
const bootstrapChunks = renderState.bootstrapChunks;
bootstrapChunks.push(startChunkForTag('template'));
pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
bootstrapChunks.push(endOfStartTag, endChunkForTag('template'));
}
}
return writeBootstrap(destination, renderState);
}
@@ -4446,7 +4400,6 @@ export function writeCompletedSegmentInstruction(
resumableState.streamingFormat === ScriptStreamingFormat;
if (scriptFormat) {
writeChunk(destination, renderState.startInlineScript);
writeChunk(destination, endOfStartTag);
if (
(resumableState.instructions & SentCompleteSegmentFunction) ===
NothingSent
@@ -4528,7 +4481,6 @@ export function writeCompletedBoundaryInstruction(
resumableState.streamingFormat === ScriptStreamingFormat;
if (scriptFormat) {
writeChunk(destination, renderState.startInlineScript);
writeChunk(destination, endOfStartTag);
if (requiresStyleInsertion) {
if (
(resumableState.instructions & SentCompleteBoundaryFunction) ===
@@ -4639,7 +4591,6 @@ export function writeClientRenderBoundaryInstruction(
resumableState.streamingFormat === ScriptStreamingFormat;
if (scriptFormat) {
writeChunk(destination, renderState.startInlineScript);
writeChunk(destination, endOfStartTag);
if (
(resumableState.instructions & SentClientRenderFunction) ===
NothingSent
@@ -4982,44 +4933,6 @@ function preloadLateStyles(this: Destination, styleQueue: StyleQueue) {
styleQueue.sheets.clear();
}
const blockingRenderChunkStart = stringToPrecomputedChunk(
'<link rel="expect" href="#',
);
const blockingRenderChunkEnd = stringToPrecomputedChunk(
'" blocking="render"/>',
);
function writeBlockingRenderInstruction(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
): void {
const idPrefix = resumableState.idPrefix;
const shellId = '\u00AB' + idPrefix + 'R\u00BB';
writeChunk(destination, blockingRenderChunkStart);
writeChunk(destination, stringToChunk(escapeTextForBrowser(shellId)));
writeChunk(destination, blockingRenderChunkEnd);
}
const completedShellIdAttributeStart = stringToPrecomputedChunk(' id="');
function pushCompletedShellIdAttribute(
target: Array<Chunk | PrecomputedChunk>,
resumableState: ResumableState,
): void {
if ((resumableState.instructions & SentCompletedShellId) !== NothingSent) {
return;
}
resumableState.instructions |= SentCompletedShellId;
const idPrefix = resumableState.idPrefix;
const shellId = '\u00AB' + idPrefix + 'R\u00BB';
target.push(
completedShellIdAttributeStart,
stringToChunk(escapeTextForBrowser(shellId)),
attributeEnd,
);
}
// We don't bother reporting backpressure at the moment because we expect to
// flush the entire preamble in a single pass. This probably should be modified
// in the future to be backpressure sensitive but that requires a larger refactor
@@ -5029,7 +4942,6 @@ export function writePreambleStart(
resumableState: ResumableState,
renderState: RenderState,
willFlushAllSegments: boolean,
skipExpect?: boolean, // Used as an override by ReactFizzConfigMarkup
): void {
// This function must be called exactly once on every request
if (
@@ -5115,16 +5027,6 @@ export function writePreambleStart(
renderState.bulkPreloads.forEach(flushResource, destination);
renderState.bulkPreloads.clear();
if ((htmlChunks || headChunks) && !skipExpect) {
// If we have any html or head chunks we know that we're rendering a full document.
// A full document should block display until the full shell has downloaded.
// Therefore we insert a render blocking instruction referring to the last body
// element that's considered part of the shell. We do this after the important loads
// have already been emitted so we don't do anything to delay them but early so that
// the browser doesn't risk painting too early.
writeBlockingRenderInstruction(destination, resumableState, renderState);
}
// Write embedding hoistableChunks
const hoistableChunks = renderState.hoistableChunks;
for (i = 0; i < hoistableChunks.length; i++) {

View File

@@ -3580,8 +3580,7 @@ describe('ReactDOMFizzServer', () => {
expect(document.head.innerHTML).toBe(
'<script type="importmap">' +
JSON.stringify(importMap) +
'</script><script async="" src="foo"></script>' +
'<link rel="expect" href="#«R»" blocking="render">',
'</script><script async="" src="foo"></script>',
);
});
@@ -4190,7 +4189,7 @@ describe('ReactDOMFizzServer', () => {
renderOptions.unstable_externalRuntimeSrc,
).map(n => n.outerHTML),
).toEqual([
'<script src="foo" id="«R»" async=""></script>',
'<script src="foo" async=""></script>',
'<script src="bar" async=""></script>',
'<script src="baz" integrity="qux" async=""></script>',
'<script type="module" src="quux" async=""></script>',
@@ -4277,7 +4276,7 @@ describe('ReactDOMFizzServer', () => {
renderOptions.unstable_externalRuntimeSrc,
).map(n => n.outerHTML),
).toEqual([
'<script src="foo" id="«R»" async=""></script>',
'<script src="foo" async=""></script>',
'<script src="bar" async=""></script>',
'<script src="baz" crossorigin="" async=""></script>',
'<script src="qux" crossorigin="" async=""></script>',
@@ -4513,7 +4512,7 @@ describe('ReactDOMFizzServer', () => {
// the html should be as-is
expect(document.documentElement.innerHTML).toEqual(
'<head><link rel="expect" href="#«R»" blocking="render"></head><body><p>hello world!</p><template id="«R»"></template></body>',
'<head></head><body><p>hello world!</p></body>',
);
});
@@ -6493,7 +6492,7 @@ describe('ReactDOMFizzServer', () => {
});
expect(document.documentElement.outerHTML).toEqual(
'<html><head><link rel="expect" href="#«R»" blocking="render"></head><body><script>try { foo() } catch (e) {} ;</script><template id="«R»"></template></body></html>',
'<html><head></head><body><script>try { foo() } catch (e) {} ;</script></body></html>',
);
});

View File

@@ -85,7 +85,7 @@ describe('ReactDOMFizzServerBrowser', () => {
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
});
@@ -99,7 +99,7 @@ describe('ReactDOMFizzServerBrowser', () => {
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script id="«R»">INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});
@@ -529,7 +529,7 @@ describe('ReactDOMFizzServerBrowser', () => {
const result = await readResult(stream);
expect(result).toEqual(
'<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/><title>foo</title></head><body>bar<template id="«R»"></template></body></html>',
'<!DOCTYPE html><html><head><title>foo</title></head><body>bar</body></html>',
);
});
@@ -547,7 +547,7 @@ describe('ReactDOMFizzServerBrowser', () => {
expect(result).toMatchInlineSnapshot(
// TODO: remove interpolation because it prevents snapshot updates.
// eslint-disable-next-line jest/no-interpolation-in-snapshots
`"<link rel="preload" as="script" fetchPriority="low" nonce="R4nd0m" href="init.js"/><link rel="modulepreload" fetchPriority="low" nonce="R4nd0m" href="init.mjs"/><div>hello world</div><script nonce="${nonce}" id="«R»">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" nonce="R4nd0m" href="init.js"/><link rel="modulepreload" fetchPriority="low" nonce="R4nd0m" href="init.mjs"/><div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
);
});

View File

@@ -72,7 +72,7 @@ describe('ReactDOMFizzServerEdge', () => {
});
expect(result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body><main>hello</main><template id="«R»"></template></body></html>"`,
`"<!DOCTYPE html><html><head></head><body><main>hello</main></body></html>"`,
);
});
});

View File

@@ -79,7 +79,7 @@ describe('ReactDOMFizzServerNode', () => {
});
// with Float, we emit empty heads if they are elided when rendering <html>
expect(output.result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
});
@@ -97,7 +97,7 @@ describe('ReactDOMFizzServerNode', () => {
pipe(writable);
});
expect(output.result).toMatchInlineSnapshot(
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script id="«R»">INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

View File

@@ -106,10 +106,7 @@ describe('ReactDOMFizzStatic', () => {
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden') &&
// Ignore the render blocking expect
(node.getAttribute('rel') !== 'expect' ||
node.getAttribute('blocking') !== 'render')
!node.hasAttribute('aria-hidden')
) {
const props = {};
const attributes = node.attributes;

View File

@@ -187,7 +187,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
);
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
});
@@ -201,7 +201,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
);
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script id="«R»">INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});
@@ -1428,8 +1428,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
expect(await readContent(content)).toBe(
'<!DOCTYPE html><html lang="en"><head>' +
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
'<link rel="expect" href="#«R»" blocking="render"/></head>' +
'<body>Hello<template id="«R»"></template></body></html>',
'</head><body>Hello</body></html>',
);
});
@@ -1475,8 +1474,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
expect(await readContent(content)).toBe(
'<!DOCTYPE html><html lang="en"><head>' +
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
'<link rel="expect" href="#«R»" blocking="render"/></head>' +
'<body>Hello<template id="«R»"></template></body></html>',
'</head><body>Hello</body></html>',
);
});
@@ -1527,8 +1525,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
expect(await readContent(content)).toBe(
'<!DOCTYPE html><html><head>' +
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
'<link rel="expect" href="#«R»" blocking="render"/></head>' +
'<body><div>Hello</div><template id="«R»"></template></body></html>',
'</head><body><div>Hello</div></body></html>',
);
});
@@ -1610,8 +1607,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
let result = decoder.decode(value, {stream: true});
expect(result).toBe(
'<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head>' +
'<body>hello<!--$?--><template id="B:1"></template><!--/$--><template id="«R»"></template>',
'<!DOCTYPE html><html><head></head><body>hello<!--$?--><template id="B:1"></template><!--/$-->',
);
await 1;
@@ -1635,9 +1631,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
const slice = result.slice(0, instructionIndex + '$RC'.length);
expect(slice).toBe(
'<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head>' +
'<body>hello<!--$?--><template id="B:1"></template><!--/$--><template id="«R»"></template>' +
'<div hidden id="S:1">world<!-- --></div><script>$RC',
'<!DOCTYPE html><html><head></head><body>hello<!--$?--><template id="B:1"></template><!--/$--><div hidden id="S:1">world<!-- --></div><script>$RC',
);
});

View File

@@ -64,7 +64,7 @@ describe('ReactDOMFizzStaticNode', () => {
);
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
});
@@ -80,7 +80,7 @@ describe('ReactDOMFizzStaticNode', () => {
);
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script id="«R»">INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

View File

@@ -250,10 +250,7 @@ describe('ReactDOMFloat', () => {
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden') &&
// Ignore the render blocking expect
(node.getAttribute('rel') !== 'expect' ||
node.getAttribute('blocking') !== 'render'))
!node.hasAttribute('aria-hidden'))
) {
const props = {};
const attributes = node.attributes;
@@ -693,9 +690,7 @@ describe('ReactDOMFloat', () => {
pipe(writable);
});
expect(chunks).toEqual([
'<!DOCTYPE html><html><head><script async="" src="foo"></script>' +
'<link rel="expect" href="#«R»" blocking="render"/><title>foo</title></head>' +
'<body>bar<template id="«R»"></template>',
'<!DOCTYPE html><html><head><script async="" src="foo"></script><title>foo</title></head><body>bar',
'</body></html>',
]);
});

View File

@@ -34,8 +34,7 @@ describe('ReactDOMFloat', () => {
);
expect(result).toEqual(
'<html><head><meta charSet="utf-8"/><link rel="expect" href="#«R»" blocking="render"/>' +
'<title>title</title><script src="foo"></script></head><template id="«R»"></template></html>',
'<html><head><meta charSet="utf-8"/><title>title</title><script src="foo"></script></head></html>',
);
});
});

View File

@@ -104,10 +104,7 @@ describe('ReactDOM HostSingleton', () => {
el.tagName !== 'TEMPLATE' &&
el.tagName !== 'template' &&
!el.hasAttribute('hidden') &&
!el.hasAttribute('aria-hidden') &&
// Ignore the render blocking expect
(node.getAttribute('rel') !== 'expect' ||
node.getAttribute('blocking') !== 'render')) ||
!el.hasAttribute('aria-hidden')) ||
el.hasAttribute('data-meaningful')
) {
const props = {};

View File

@@ -77,16 +77,12 @@ describe('rendering React components at document', () => {
await act(() => {
root = ReactDOMClient.hydrateRoot(testDocument, <Root hello="world" />);
});
expect(testDocument.body.innerHTML).toBe(
'Hello world' + '<template id="«R»"></template>',
);
expect(testDocument.body.innerHTML).toBe('Hello world');
await act(() => {
root.render(<Root hello="moon" />);
});
expect(testDocument.body.innerHTML).toBe(
'Hello moon' + '<template id="«R»"></template>',
);
expect(testDocument.body.innerHTML).toBe('Hello moon');
expect(body === testDocument.body).toBe(true);
});
@@ -111,9 +107,7 @@ describe('rendering React components at document', () => {
await act(() => {
root = ReactDOMClient.hydrateRoot(testDocument, <Root />);
});
expect(testDocument.body.innerHTML).toBe(
'Hello world' + '<template id="«R»"></template>',
);
expect(testDocument.body.innerHTML).toBe('Hello world');
const originalDocEl = testDocument.documentElement;
const originalHead = testDocument.head;
@@ -124,10 +118,8 @@ describe('rendering React components at document', () => {
expect(testDocument.firstChild).toBe(originalDocEl);
expect(testDocument.head).toBe(originalHead);
expect(testDocument.body).toBe(originalBody);
expect(originalBody.innerHTML).toBe('<template id="«R»"></template>');
expect(originalHead.innerHTML).toBe(
'<link rel="expect" href="#«R»" blocking="render">',
);
expect(originalBody.firstChild).toEqual(null);
expect(originalHead.firstChild).toEqual(null);
});
it('should not be able to switch root constructors', async () => {
@@ -165,17 +157,13 @@ describe('rendering React components at document', () => {
root = ReactDOMClient.hydrateRoot(testDocument, <Component />);
});
expect(testDocument.body.innerHTML).toBe(
'Hello world' + '<template id="«R»"></template>',
);
expect(testDocument.body.innerHTML).toBe('Hello world');
await act(() => {
root.render(<Component2 />);
});
expect(testDocument.body.innerHTML).toBe(
'<template id="«R»"></template>' + 'Goodbye world',
);
expect(testDocument.body.innerHTML).toBe('Goodbye world');
});
it('should be able to mount into document', async () => {
@@ -204,9 +192,7 @@ describe('rendering React components at document', () => {
);
});
expect(testDocument.body.innerHTML).toBe(
'Hello world' + '<template id="«R»"></template>',
);
expect(testDocument.body.innerHTML).toBe('Hello world');
});
it('cannot render over an existing text child at the root', async () => {
@@ -339,9 +325,7 @@ describe('rendering React components at document', () => {
: [],
);
expect(testDocument.body.innerHTML).toBe(
favorSafetyOverHydrationPerf
? 'Hello world'
: 'Goodbye world<template id="«R»"></template>',
favorSafetyOverHydrationPerf ? 'Hello world' : 'Goodbye world',
);
});

View File

@@ -150,10 +150,7 @@ function getVisibleChildren(element: Element): React$Node {
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden') &&
// Ignore the render blocking expect
(node.getAttribute('rel') !== 'expect' ||
node.getAttribute('blocking') !== 'render')
!node.hasAttribute('aria-hidden')
) {
const props: any = {};
const attributes = node.attributes;

View File

@@ -17,10 +17,7 @@ import type {
FormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
import {
pushStartInstance as pushStartInstanceImpl,
writePreambleStart as writePreambleStartImpl,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
import {pushStartInstance as pushStartInstanceImpl} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
import type {
Destination,
@@ -65,11 +62,13 @@ export {
writeEndPendingSuspenseBoundary,
writeHoistablesForBoundary,
writePlaceholder,
writeCompletedRoot,
createRootFormatContext,
createRenderState,
createResumableState,
createPreambleState,
createHoistableState,
writePreambleStart,
writePreambleEnd,
writeHoistables,
writePostamble,
@@ -204,30 +203,5 @@ export function writeEndClientRenderedSuspenseBoundary(
return true;
}
export function writePreambleStart(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
willFlushAllSegments: boolean,
skipExpect?: boolean, // Used as an override by ReactFizzConfigMarkup
): void {
return writePreambleStartImpl(
destination,
resumableState,
renderState,
willFlushAllSegments,
true, // skipExpect
);
}
export function writeCompletedRoot(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
): boolean {
// Markup doesn't have any bootstrap scripts nor shell completions.
return true;
}
export type TransitionStatus = FormStatus;
export const NotPendingTransition: TransitionStatus = NotPending;

View File

@@ -1062,9 +1062,9 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
if (
parent !== null &&
parent.tag === SuspenseComponent &&
parent.memoizedProps.name
parent.memoizedProps.unstable_name
) {
name = parent.memoizedProps.name;
name = parent.memoizedProps.unstable_name;
}
if (!wasHidden && isHidden) {
@@ -4952,7 +4952,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
if (transitions !== null) {
const abortReason = {
reason: 'suspense',
name: current.memoizedProps.name || null,
name: current.memoizedProps.unstable_name || null,
};
if (
current.memoizedState === null ||

View File

@@ -412,7 +412,7 @@ describe('ReactInteractionTracing', () => {
{navigate ? (
<Suspense
fallback={<Text text="Loading..." />}
name="suspense page">
unstable_name="suspense page">
<AsyncText text="Page Two" />
</Suspense>
) : (
@@ -498,14 +498,14 @@ describe('ReactInteractionTracing', () => {
<>
{showText ? (
<Suspense
name="show text"
unstable_name="show text"
fallback={<Text text="Show Text Loading..." />}>
<AsyncText text="Show Text" />
</Suspense>
) : null}
<Suspense
fallback={<Text text="Loading..." />}
name="suspense page">
unstable_name="suspense page">
<AsyncText text="Page Two" />
</Suspense>
</>
@@ -605,14 +605,14 @@ describe('ReactInteractionTracing', () => {
<>
{showText ? (
<Suspense
name="show text"
unstable_name="show text"
fallback={<Text text="Show Text Loading..." />}>
<AsyncText text="Show Text" />
</Suspense>
) : null}
<Suspense
fallback={<Text text="Loading..." />}
name="suspense page">
unstable_name="suspense page">
<AsyncText text="Page Two" />
</Suspense>
</>
@@ -719,16 +719,16 @@ describe('ReactInteractionTracing', () => {
<>
<Suspense
fallback={<Text text="Loading..." />}
name="suspense page">
unstable_name="suspense page">
<AsyncText text="Page Two" />
<Suspense
name="show text one"
unstable_name="show text one"
fallback={<Text text="Show Text One Loading..." />}>
<AsyncText text="Show Text One" />
</Suspense>
<div>
<Suspense
name="show text two"
unstable_name="show text two"
fallback={<Text text="Show Text Two Loading..." />}>
<AsyncText text="Show Text Two" />
</Suspense>
@@ -848,12 +848,12 @@ describe('ReactInteractionTracing', () => {
<>
<Suspense
fallback={<Text text="Loading..." />}
name="suspense page">
unstable_name="suspense page">
<AsyncText text="Page Two" />
{/* showTextOne is entangled with navigate */}
{showTextOne ? (
<Suspense
name="show text one"
unstable_name="show text one"
fallback={<Text text="Show Text One Loading..." />}>
<AsyncText text="Show Text One" />
</Suspense>
@@ -865,7 +865,7 @@ describe('ReactInteractionTracing', () => {
from completing */}
{showTextTwo ? (
<Suspense
name="show text two"
unstable_name="show text two"
fallback={<Text text="Show Text Two Loading..." />}>
<AsyncText text="Show Text Two" />
</Suspense>
@@ -1115,13 +1115,13 @@ describe('ReactInteractionTracing', () => {
{navigate ? (
<Suspense
fallback={<Text text="Loading..." />}
name="suspense page">
unstable_name="suspense page">
<AsyncText text="Page Two" />
<React.unstable_TracingMarker name="sync marker" />
<React.unstable_TracingMarker name="async marker">
<Suspense
fallback={<Text text="Loading..." />}
name="marker suspense">
unstable_name="marker suspense">
<AsyncText text="Marker Text" />
</Suspense>
</React.unstable_TracingMarker>
@@ -1226,18 +1226,20 @@ describe('ReactInteractionTracing', () => {
<div>
{navigate ? (
<React.unstable_TracingMarker name="outer marker">
<Suspense fallback={<Text text="Outer..." />} name="outer">
<Suspense
fallback={<Text text="Outer..." />}
unstable_name="outer">
<AsyncText text="Outer Text" />
<Suspense
fallback={<Text text="Inner One..." />}
name="inner one">
unstable_name="inner one">
<React.unstable_TracingMarker name="marker one">
<AsyncText text="Inner Text One" />
</React.unstable_TracingMarker>
</Suspense>
<Suspense
fallback={<Text text="Inner Two..." />}
name="inner two">
unstable_name="inner two">
<React.unstable_TracingMarker name="marker two">
<AsyncText text="Inner Text Two" />
</React.unstable_TracingMarker>
@@ -1486,21 +1488,21 @@ describe('ReactInteractionTracing', () => {
{showMarker ? (
<React.unstable_TracingMarker name="marker one">
<Suspense
name="suspense page"
unstable_name="suspense page"
fallback={<Text text="Loading..." />}>
<AsyncText text="Page Two" />
</Suspense>
</React.unstable_TracingMarker>
) : (
<Suspense
name="suspense page"
unstable_name="suspense page"
fallback={<Text text="Loading..." />}>
<AsyncText text="Page Two" />
</Suspense>
)}
<React.unstable_TracingMarker name="sibling">
<Suspense
name="suspense sibling"
unstable_name="suspense sibling"
fallback={<Text text="Sibling Loading..." />}>
<AsyncText text="Sibling Text" />
</Suspense>
@@ -1650,7 +1652,7 @@ describe('ReactInteractionTracing', () => {
<div>
<React.unstable_TracingMarker name="one">
<Suspense
name="suspense one"
unstable_name="suspense one"
fallback={<Text text="Loading One..." />}>
<AsyncText text="Page One" />
</Suspense>
@@ -1659,7 +1661,7 @@ describe('ReactInteractionTracing', () => {
) : null}
<React.unstable_TracingMarker name="two">
<Suspense
name="suspense two"
unstable_name="suspense two"
fallback={<Text text="Loading Two..." />}>
<AsyncText text="Page Two" />
</Suspense>
@@ -1786,12 +1788,12 @@ describe('ReactInteractionTracing', () => {
<React.unstable_TracingMarker name="one">
{!deleteOne ? (
<Suspense
name="suspense one"
unstable_name="suspense one"
fallback={<Text text="Loading One..." />}>
<AsyncText text="Page One" />
<React.unstable_TracingMarker name="page one" />
<Suspense
name="suspense child"
unstable_name="suspense child"
fallback={<Text text="Loading Child..." />}>
<React.unstable_TracingMarker name="child" />
<AsyncText text="Child" />
@@ -1801,7 +1803,7 @@ describe('ReactInteractionTracing', () => {
</React.unstable_TracingMarker>
<React.unstable_TracingMarker name="two">
<Suspense
name="suspense two"
unstable_name="suspense two"
fallback={<Text text="Loading Two..." />}>
<AsyncText text="Page Two" />
</Suspense>
@@ -1946,11 +1948,11 @@ describe('ReactInteractionTracing', () => {
return (
<React.unstable_TracingMarker name="parent">
{show ? (
<Suspense name="appended child">
<Suspense unstable_name="appended child">
<AsyncText text="Appended child" />
</Suspense>
) : null}
<Suspense name="child">
<Suspense unstable_name="child">
<AsyncText text="Child" />
</Suspense>
</React.unstable_TracingMarker>
@@ -2066,13 +2068,13 @@ describe('ReactInteractionTracing', () => {
{show ? (
<React.unstable_TracingMarker name="appended child">
{showSuspense ? (
<Suspense name="appended child">
<Suspense unstable_name="appended child">
<AsyncText text="Appended child" />
</Suspense>
) : null}
</React.unstable_TracingMarker>
) : null}
<Suspense name="child">
<Suspense unstable_name="child">
<AsyncText text="Child" />
</Suspense>
</React.unstable_TracingMarker>
@@ -2347,7 +2349,9 @@ describe('ReactInteractionTracing', () => {
function App() {
return (
<Suspense fallback={<Text text="Loading..." />} name="suspense page">
<Suspense
fallback={<Text text="Loading..." />}
unstable_name="suspense page">
<AsyncText text="Page Two" />
</Suspense>
);
@@ -2412,10 +2416,12 @@ describe('ReactInteractionTracing', () => {
});
return (
<>
<Suspense name="one" fallback={<Text text="Loading..." />}>
<Suspense unstable_name="one" fallback={<Text text="Loading..." />}>
<AsyncText text="Text" />
</Suspense>
<Suspense name="two" fallback={<Text text="Loading Two..." />}>
<Suspense
unstable_name="two"
fallback={<Text text="Loading Two..." />}>
<AsyncText text="Text Two" />
</Suspense>
</>
@@ -2484,7 +2490,9 @@ describe('ReactInteractionTracing', () => {
function App({name}) {
return (
<>
<Suspense name={name} fallback={<Text text={`Loading ${name}...`} />}>
<Suspense
unstable_name={name}
fallback={<Text text={`Loading ${name}...`} />}>
<AsyncText text={`Text ${name}`} />
</Suspense>
</>

View File

@@ -59,7 +59,7 @@ describe('ReactDOMServerFB', () => {
});
const result = readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script id="«R»">INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

View File

@@ -193,10 +193,7 @@ describe('ReactFlightDOM', () => {
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden') &&
// Ignore the render blocking expect
(node.getAttribute('rel') !== 'expect' ||
node.getAttribute('blocking') !== 'render'))
!node.hasAttribute('aria-hidden'))
) {
const props = {};
const attributes = node.attributes;
@@ -1920,15 +1917,11 @@ describe('ReactFlightDOM', () => {
expect(content1).toEqual(
'<!DOCTYPE html><html><head><link rel="preload" href="before1" as="style"/>' +
'<link rel="preload" href="after1" as="style"/>' +
'<link rel="expect" href="#«R»" blocking="render"/></head>' +
'<body><p>hello world</p><template id="«R»"></template></body></html>',
'<link rel="preload" href="after1" as="style"/></head><body><p>hello world</p></body></html>',
);
expect(content2).toEqual(
'<!DOCTYPE html><html><head><link rel="preload" href="before2" as="style"/>' +
'<link rel="preload" href="after2" as="style"/>' +
'<link rel="expect" href="#«R»" blocking="render"/></head>' +
'<body><p>hello world</p><template id="«R»"></template></body></html>',
'<link rel="preload" href="after2" as="style"/></head><body><p>hello world</p></body></html>',
);
});

View File

@@ -1899,8 +1899,8 @@ describe('ReactFlightDOMBrowser', () => {
}
expect(content).toEqual(
'<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head>' +
'<body><p>hello world</p><template id="«R»"></template></body></html>',
'<!DOCTYPE html><html><head>' +
'</head><body><p>hello world</p></body></html>',
);
});

View File

@@ -5157,11 +5157,7 @@ function flushCompletedQueues(
);
flushSegment(request, destination, completedRootSegment, null);
request.completedRootSegment = null;
writeCompletedRoot(
destination,
request.resumableState,
request.renderState,
);
writeCompletedRoot(destination, request.renderState);
}
writeHoistables(destination, request.resumableState, request.renderState);

View File

@@ -278,7 +278,7 @@ export type SuspenseProps = {
unstable_avoidThisFallback?: boolean,
unstable_expectedLoadTime?: number,
name?: string,
unstable_name?: string,
};
export type TracingMarkerProps = {

View File

@@ -393,8 +393,7 @@ function getPlugins(
};
},
},
// See https://github.com/rollup/plugins/issues/1425
bundle.tsconfig != null ? commonjs({strictRequires: true}) : false,
bundle.tsconfig != null ? commonjs() : false,
// Shim any modules that need forking in this environment.
useForks(forks),
// Ensure we don't try to bundle any fbjs modules.