mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Update base for Update on "compiler: super early exploration of instruction reordering"
See comments in InstructionReordering.ts. This needs substantial iteration before landing in some form, just putting up to share for discussion. [ghstack-poisoned]
This commit is contained in:
@@ -507,6 +507,10 @@ workflows:
|
||||
- "-r=www-modern --env=production --variant=false"
|
||||
- "-r=www-modern --env=development --variant=true"
|
||||
- "-r=www-modern --env=production --variant=true"
|
||||
- "-r=xplat --env=development --variant=false"
|
||||
- "-r=xplat --env=development --variant=true"
|
||||
- "-r=xplat --env=production --variant=false"
|
||||
- "-r=xplat --env=production --variant=true"
|
||||
|
||||
# TODO: Test more persistent configurations?
|
||||
- '-r=stable --env=development --persistent'
|
||||
@@ -552,6 +556,12 @@ workflows:
|
||||
# - "-r=www-modern --env=development --variant=true"
|
||||
# - "-r=www-modern --env=production --variant=true"
|
||||
|
||||
# TODO: Update test config to support xplat build tests
|
||||
# - "-r=xplat --env=development --variant=false"
|
||||
# - "-r=xplat --env=development --variant=true"
|
||||
# - "-r=xplat --env=production --variant=false"
|
||||
# - "-r=xplat --env=production --variant=true"
|
||||
|
||||
# TODO: Test more persistent configurations?
|
||||
- download_base_build_for_sizebot:
|
||||
filters:
|
||||
|
||||
@@ -486,6 +486,7 @@ module.exports = {
|
||||
$ReadOnlyArray: 'readonly',
|
||||
$ArrayBufferView: 'readonly',
|
||||
$Shape: 'readonly',
|
||||
CallSite: 'readonly',
|
||||
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
|
||||
ReturnType: 'readonly',
|
||||
AnimationFrameID: 'readonly',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📃 Documentation Issue
|
||||
url: https://github.com/reactjs/react.dev/issues/new/choose
|
||||
|
||||
@@ -10,7 +10,36 @@ jobs:
|
||||
outputs:
|
||||
www_branch_count: ${{ steps.check_branches.outputs.www_branch_count }}
|
||||
fbsource_branch_count: ${{ steps.check_branches.outputs.fbsource_branch_count }}
|
||||
last_version_classic: ${{ steps.get_last_version_www.outputs.last_version_classic }}
|
||||
last_version_modern: ${{ steps.get_last_version_www.outputs.last_version_modern }}
|
||||
last_version_rn: ${{ steps.get_last_version_rn.outputs.last_version_rn }}
|
||||
current_version_classic: ${{ steps.get_current_version.outputs.current_version_classic }}
|
||||
current_version_modern: ${{ steps.get_current_version.outputs.current_version_modern }}
|
||||
current_version_rn: ${{ steps.get_current_version.outputs.current_version_rn }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: builds/facebook-www
|
||||
- name: "Get last version string for www"
|
||||
id: get_last_version_www
|
||||
run: |
|
||||
# Empty checks only needed for backwards compatibility,can remove later.
|
||||
VERSION_CLASSIC=$( [ -f ./compiled/facebook-www/VERSION_CLASSIC ] && cat ./compiled/facebook-www/VERSION_CLASSIC || echo '' )
|
||||
VERSION_MODERN=$( [ -f ./compiled/facebook-www/VERSION_MODERN ] && cat ./compiled/facebook-www/VERSION_MODERN || echo '' )
|
||||
echo "Last classic version is $VERSION_CLASSIC"
|
||||
echo "Last modern version is $VERSION_MODERN"
|
||||
echo "last_version_classic=$VERSION_CLASSIC" >> "$GITHUB_OUTPUT"
|
||||
echo "last_version_modern=$VERSION_MODERN" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: builds/facebook-fbsource
|
||||
- name: "Get last version string for rn"
|
||||
id: get_last_version_rn
|
||||
run: |
|
||||
# Empty checks only needed for backwards compatibility,can remove later.
|
||||
VERSION_NATIVE_FB=$( [ -f ./compiled-rn/VERSION_NATIVE_FB ] && cat ./compiled-rn/VERSION_NATIVE_FB || echo '' )
|
||||
echo "Last rn version is $VERSION_NATIVE_FB"
|
||||
echo "last_version_rn=$VERSION_NATIVE_FB" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Check branches"
|
||||
id: check_branches
|
||||
@@ -147,7 +176,7 @@ jobs:
|
||||
mkdir -p ${BASE_FOLDER}/react-native-github/Libraries/Renderer/
|
||||
mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-is,react-test-renderer}/
|
||||
|
||||
# Move React Native renderer
|
||||
# Move React Native renderer
|
||||
mv build/react-native/implementations/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
|
||||
mv build/react-native/shims/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
|
||||
mv build/facebook-react-native/scheduler/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/scheduler/
|
||||
@@ -160,11 +189,27 @@ jobs:
|
||||
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
|
||||
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
|
||||
|
||||
ls -R ./compiled
|
||||
- name: Add REVISION file
|
||||
# Move React Native version file
|
||||
mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB
|
||||
|
||||
ls -R ./compiled-rn
|
||||
- name: Add REVISION files
|
||||
run: |
|
||||
echo ${{ github.sha }} >> ./compiled/facebook-www/REVISION
|
||||
cp ./compiled/facebook-www/REVISION ./compiled/facebook-www/REVISION_TRANSFORMS
|
||||
echo ${{ github.sha }} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
|
||||
- name: "Get current version string"
|
||||
id: get_current_version
|
||||
run: |
|
||||
VERSION_CLASSIC=$(cat ./compiled/facebook-www/VERSION_CLASSIC)
|
||||
VERSION_MODERN=$(cat ./compiled/facebook-www/VERSION_MODERN)
|
||||
VERSION_NATIVE_FB=$(cat ./compiled-rn/VERSION_NATIVE_FB)
|
||||
echo "Current classic version is $VERSION_CLASSIC"
|
||||
echo "Current modern version is $VERSION_MODERN"
|
||||
echo "Current rn version is $VERSION_NATIVE_FB"
|
||||
echo "current_version_classic=$VERSION_CLASSIC" >> "$GITHUB_OUTPUT"
|
||||
echo "current_version_modern=$VERSION_MODERN" >> "$GITHUB_OUTPUT"
|
||||
echo "current_version_rn=$VERSION_NATIVE_FB" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: compiled
|
||||
@@ -188,15 +233,58 @@ jobs:
|
||||
with:
|
||||
name: compiled
|
||||
path: compiled/
|
||||
- run: git status -u
|
||||
- name: Revert version changes
|
||||
if: needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != ''
|
||||
env:
|
||||
CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }}
|
||||
CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }}
|
||||
LAST_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.last_version_classic }}
|
||||
LAST_VERSION_MODERN: ${{ needs.download_artifacts.outputs.last_version_modern }}
|
||||
run: |
|
||||
echo "Reverting $CURRENT_VERSION_CLASSIC to $LAST_VERSION_CLASSIC"
|
||||
grep -rl "$CURRENT_VERSION_CLASSIC" ./compiled || echo "No files found with $CURRENT_VERSION_CLASSIC"
|
||||
grep -rl "$CURRENT_VERSION_CLASSIC" ./compiled | xargs -r sed -i -e "s/$CURRENT_VERSION_CLASSIC/$LAST_VERSION_CLASSIC/g"
|
||||
grep -rl "$CURRENT_VERSION_CLASSIC" ./compiled || echo "Classic version reverted"
|
||||
echo "===================="
|
||||
echo "Reverting $CURRENT_VERSION_MODERN to $LAST_VERSION_MODERN"
|
||||
grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "No files found with $CURRENT_VERSION_MODERN"
|
||||
grep -rl "$CURRENT_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$CURRENT_VERSION_MODERN/$LAST_VERSION_MODERN/g"
|
||||
grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "Modern version reverted"
|
||||
- name: Check if only the REVISION file has changed
|
||||
id: check_should_commit
|
||||
run: |
|
||||
if git status --porcelain | grep -qv '/REVISION$'; then
|
||||
echo "Full git status"
|
||||
git status
|
||||
echo "===================="
|
||||
if git status --porcelain | grep -qv '/REVISION'; then
|
||||
echo "Changes detected"
|
||||
echo "should_commit=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No Changes detected"
|
||||
echo "should_commit=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Re-apply version changes
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != ''
|
||||
env:
|
||||
CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }}
|
||||
CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }}
|
||||
LAST_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.last_version_classic }}
|
||||
LAST_VERSION_MODERN: ${{ needs.download_artifacts.outputs.last_version_modern }}
|
||||
run: |
|
||||
echo "Re-applying $LAST_VERSION_CLASSIC to $CURRENT_VERSION_CLASSIC"
|
||||
grep -rl "$LAST_VERSION_CLASSIC" ./compiled || echo "No files found with $LAST_VERSION_CLASSIC"
|
||||
grep -rl "$LAST_VERSION_CLASSIC" ./compiled | xargs -r sed -i -e "s/$LAST_VERSION_CLASSIC/$CURRENT_VERSION_CLASSIC/g"
|
||||
grep -rl "$LAST_VERSION_CLASSIC" ./compiled || echo "Classic version re-applied"
|
||||
echo "===================="
|
||||
echo "Re-applying $LAST_VERSION_MODERN to $CURRENT_VERSION_MODERN"
|
||||
grep -rl "$LAST_VERSION_MODERN" ./compiled || echo "No files found with $LAST_VERSION_MODERN"
|
||||
grep -rl "$LAST_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$LAST_VERSION_MODERN/$CURRENT_VERSION_MODERN/g"
|
||||
grep -rl "$LAST_VERSION_MODERN" ./compiled || echo "Classic version re-applied"
|
||||
- name: Will commit these changes
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
run: |
|
||||
echo ":"
|
||||
git status -u
|
||||
- name: Commit changes to branch
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
@@ -219,20 +307,52 @@ jobs:
|
||||
with:
|
||||
ref: builds/facebook-fbsource
|
||||
- name: Ensure clean directory
|
||||
run: rm -rf compiled
|
||||
run: rm -rf compiled-rn
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: compiled-rn
|
||||
path: compiled-rn/
|
||||
- run: git status -u
|
||||
- name: Revert version changes
|
||||
if: needs.download_artifacts.outputs.last_version_rn != ''
|
||||
env:
|
||||
CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }}
|
||||
LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }}
|
||||
run: |
|
||||
echo "Reverting $CURRENT_VERSION to $LAST_VERSION"
|
||||
grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "No files found with $CURRENT_VERSION"
|
||||
grep -rl "$CURRENT_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$CURRENT_VERSION/$LAST_VERSION/g"
|
||||
grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "Version reverted"
|
||||
- name: Check if only the REVISION file has changed
|
||||
id: check_should_commit
|
||||
run: |
|
||||
if git status --porcelain | grep -qv '/REVISION$'; then
|
||||
echo "Full git status"
|
||||
git status
|
||||
echo "===================="
|
||||
echo "Checking for changes"
|
||||
# Check if there are changes in the files other than REVISION or @generated headers
|
||||
# We also filter out the file name lines with "---" and "+++".
|
||||
if git diff -- . ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|@generated SignedSource)" | grep "^[+-]" > /dev/null; then
|
||||
echo "Changes detected"
|
||||
echo "should_commit=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No Changes detected"
|
||||
echo "should_commit=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Re-apply version changes
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != ''
|
||||
env:
|
||||
CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }}
|
||||
LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }}
|
||||
run: |
|
||||
echo "Re-applying $LAST_VERSION to $CURRENT_VERSION"
|
||||
grep -rl "$LAST_VERSION" ./compiled-rn || echo "No files found with $LAST_VERSION"
|
||||
grep -rl "$LAST_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$LAST_VERSION/$CURRENT_VERSION/g"
|
||||
grep -rl "$LAST_VERSION" ./compiled-rn || echo "Version re-applied"
|
||||
- name: Will commit these changes
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
run: |
|
||||
echo ":"
|
||||
git status -u
|
||||
- name: Commit changes to branch
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
|
||||
@@ -28,6 +28,10 @@ const ReactVersion = '19.0.0';
|
||||
// npm dist tags used during publish, refer to .circleci/config.yml.
|
||||
const canaryChannelLabel = 'rc';
|
||||
|
||||
// If the canaryChannelLabel is "rc", the build pipeline will use this to build
|
||||
// an RC version of the packages.
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '5.1.0',
|
||||
'jest-react': '0.16.0',
|
||||
@@ -53,6 +57,7 @@ const experimentalPackages = [];
|
||||
module.exports = {
|
||||
ReactVersion,
|
||||
canaryChannelLabel,
|
||||
rcNumber,
|
||||
stablePackages,
|
||||
experimentalPackages,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "babel-plugin-react-compiler",
|
||||
"version": "0.0.0-experimental-487cb0e-20240529",
|
||||
"version": "0.0.0-experimental-938cd9a-20240601",
|
||||
"description": "Babel plugin for React Compiler.",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -30,10 +30,16 @@ export default function BabelPluginReactCompiler(
|
||||
*/
|
||||
Program(prog, pass): void {
|
||||
let opts = parsePluginOptions(pass.opts);
|
||||
if (pipelineUsesReanimatedPlugin(pass.file.opts.plugins)) {
|
||||
const isDev =
|
||||
(typeof __DEV__ !== "undefined" && __DEV__ === true) ||
|
||||
process.env["NODE_ENV"] === "development";
|
||||
if (
|
||||
opts.enableReanimatedCheck === true &&
|
||||
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
|
||||
) {
|
||||
opts = injectReanimatedFlag(opts);
|
||||
}
|
||||
if (process.env["NODE_ENV"] === "development") {
|
||||
if (isDev) {
|
||||
opts = {
|
||||
...opts,
|
||||
environment: {
|
||||
|
||||
@@ -111,6 +111,12 @@ export type PluginOptions = {
|
||||
ignoreUseNoForget: boolean;
|
||||
|
||||
sources?: Array<string> | ((filename: string) => boolean) | null;
|
||||
|
||||
/**
|
||||
* The compiler has customized support for react-native-reanimated, intended as a temporary workaround.
|
||||
* Set this flag (on by default) to automatically check for this library and activate the support.
|
||||
*/
|
||||
enableReanimatedCheck: boolean;
|
||||
};
|
||||
|
||||
const CompilationModeSchema = z.enum([
|
||||
@@ -188,6 +194,7 @@ export const defaultOptions: PluginOptions = {
|
||||
sources: (filename) => {
|
||||
return filename.indexOf("node_modules") === -1;
|
||||
},
|
||||
enableReanimatedCheck: true,
|
||||
} as const;
|
||||
|
||||
export function parsePluginOptions(obj: unknown): PluginOptions {
|
||||
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
validatePreservedManualMemoization,
|
||||
validateUseMemo,
|
||||
} from "../Validation";
|
||||
import pruneInitializationDependencies from "../ReactiveScopes/PruneInitializationDependencies";
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| { kind: "ast"; name: string; value: CodegenFunction }
|
||||
@@ -147,8 +148,14 @@ function* runWithEnvironment(
|
||||
validateContextVariableLValues(hir);
|
||||
validateUseMemo(hir);
|
||||
|
||||
dropManualMemoization(hir);
|
||||
yield log({ kind: "hir", name: "DropManualMemoization", value: hir });
|
||||
if (
|
||||
!env.config.enablePreserveExistingManualUseMemo &&
|
||||
!env.config.disableMemoizationForDebugging &&
|
||||
!env.config.enableChangeDetectionForDebugging
|
||||
) {
|
||||
dropManualMemoization(hir);
|
||||
yield log({ kind: "hir", name: "DropManualMemoization", value: hir });
|
||||
}
|
||||
|
||||
inlineImmediatelyInvokedFunctionExpressions(hir);
|
||||
yield log({
|
||||
@@ -373,6 +380,15 @@ function* runWithEnvironment(
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
if (env.config.enableChangeDetectionForDebugging != null) {
|
||||
pruneInitializationDependencies(reactiveFunction);
|
||||
yield log({
|
||||
kind: "reactive",
|
||||
name: "PruneInitializationDependencies",
|
||||
value: reactiveFunction,
|
||||
});
|
||||
}
|
||||
|
||||
propagateEarlyReturns(reactiveFunction);
|
||||
yield log({
|
||||
kind: "reactive",
|
||||
|
||||
@@ -421,6 +421,13 @@ export function compileProgram(
|
||||
);
|
||||
externalFunctions.push(enableEmitHookGuards);
|
||||
}
|
||||
|
||||
if (options.environment?.enableChangeDetectionForDebugging != null) {
|
||||
const enableChangeDetectionForDebugging = tryParseExternalFunction(
|
||||
options.environment.enableChangeDetectionForDebugging
|
||||
);
|
||||
externalFunctions.push(enableChangeDetectionForDebugging);
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err, pass, null);
|
||||
return;
|
||||
|
||||
@@ -124,7 +124,7 @@ export function lower(
|
||||
) {
|
||||
const place: Place = {
|
||||
kind: "Identifier",
|
||||
identifier: builder.makeTemporary(),
|
||||
identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
|
||||
effect: Effect.Unknown,
|
||||
reactive: false,
|
||||
loc: param.node.loc ?? GeneratedSource,
|
||||
@@ -141,7 +141,7 @@ export function lower(
|
||||
} else if (param.isRestElement()) {
|
||||
const place: Place = {
|
||||
kind: "Identifier",
|
||||
identifier: builder.makeTemporary(),
|
||||
identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
|
||||
effect: Effect.Unknown,
|
||||
reactive: false,
|
||||
loc: param.node.loc ?? GeneratedSource,
|
||||
@@ -1256,7 +1256,9 @@ function lowerStatement(
|
||||
if (hasNode(handlerBindingPath)) {
|
||||
const place: Place = {
|
||||
kind: "Identifier",
|
||||
identifier: builder.makeTemporary(),
|
||||
identifier: builder.makeTemporary(
|
||||
handlerBindingPath.node.loc ?? GeneratedSource
|
||||
),
|
||||
effect: Effect.Unknown,
|
||||
reactive: false,
|
||||
loc: handlerBindingPath.node.loc ?? GeneratedSource,
|
||||
@@ -3301,7 +3303,7 @@ function lowerIdentifier(
|
||||
function buildTemporaryPlace(builder: HIRBuilder, loc: SourceLocation): Place {
|
||||
const place: Place = {
|
||||
kind: "Identifier",
|
||||
identifier: builder.makeTemporary(),
|
||||
identifier: builder.makeTemporary(loc),
|
||||
effect: Effect.Unknown,
|
||||
reactive: false,
|
||||
loc,
|
||||
|
||||
@@ -165,6 +165,13 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* When this is true, rather than pruning existing manual memoization but ensuring or validating
|
||||
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
|
||||
* useMemo/useCallback.
|
||||
*/
|
||||
enablePreserveExistingManualUseMemo: z.boolean().default(false),
|
||||
|
||||
// 🌲
|
||||
enableForest: z.boolean().default(false),
|
||||
|
||||
@@ -343,6 +350,23 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableTreatFunctionDepsAsConditional: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When true, always act as though the dependencies of a memoized value
|
||||
* have changed. This makes the compiler not actually perform any optimizations,
|
||||
* but is useful for debugging. Implicitly also sets
|
||||
* @enablePreserveExistingManualUseMemo, because otherwise memoization in the
|
||||
* original source will be disabled as well.
|
||||
*/
|
||||
disableMemoizationForDebugging: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When true, rather using memoized values, the compiler will always re-compute
|
||||
* values, and then use a heuristic to compare the memoized value to the newly
|
||||
* computed one. This detects cases where rules of react violations may cause the
|
||||
* compiled code to behave differently than the original.
|
||||
*/
|
||||
enableChangeDetectionForDebugging: ExternalFunctionSchema.nullish(),
|
||||
|
||||
/**
|
||||
* The react native re-animated library uses custom Babel transforms that
|
||||
* requires the calls to library API remain unmodified.
|
||||
@@ -462,6 +486,18 @@ export class Environment {
|
||||
this.#shapes = new Map(DEFAULT_SHAPES);
|
||||
this.#globals = new Map(DEFAULT_GLOBALS);
|
||||
|
||||
if (
|
||||
config.disableMemoizationForDebugging &&
|
||||
config.enableChangeDetectionForDebugging != null
|
||||
) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Invalid environment config: the 'disableMemoizationForDebugging' and 'enableChangeDetectionForDebugging' options cannot be used together`,
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [hookName, hook] of this.config.customHooks) {
|
||||
CompilerError.invariant(!this.#globals.has(hookName), {
|
||||
reason: `[Globals] Found existing definition in global registry for custom hook ${hookName}`,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
BuiltInUseInsertionEffectHookId,
|
||||
BuiltInUseLayoutEffectHookId,
|
||||
BuiltInUseOperatorId,
|
||||
BuiltInUseReducerId,
|
||||
BuiltInUseRefId,
|
||||
BuiltInUseStateId,
|
||||
ShapeRegistry,
|
||||
@@ -265,6 +266,18 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
returnValueReason: ValueReason.State,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"useReducer",
|
||||
addHook(DEFAULT_SHAPES, {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: { kind: "Object", shapeId: BuiltInUseReducerId },
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: "useReducer",
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
returnValueReason: ValueReason.ReducerState,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"useRef",
|
||||
addHook(DEFAULT_SHAPES, {
|
||||
|
||||
@@ -1144,6 +1144,7 @@ export type Identifier = {
|
||||
*/
|
||||
scope: ReactiveScope | null;
|
||||
type: Type;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
export type IdentifierName = ValidatedIdentifier | PromotedIdentifier;
|
||||
@@ -1253,6 +1254,11 @@ export enum ValueReason {
|
||||
*/
|
||||
State = "state",
|
||||
|
||||
/**
|
||||
* A value returned from `useReducer`
|
||||
*/
|
||||
ReducerState = "reducer-state",
|
||||
|
||||
/**
|
||||
* Props of a component or arguments of a hook.
|
||||
*/
|
||||
@@ -1376,6 +1382,8 @@ export type ReactiveScope = {
|
||||
* no longer exist due to being pruned.
|
||||
*/
|
||||
merged: Set<ScopeId>;
|
||||
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
export type ReactiveScopeDependencies = Set<ReactiveScopeDependency>;
|
||||
@@ -1490,6 +1498,14 @@ export function isSetStateType(id: Identifier): boolean {
|
||||
return id.type.kind === "Function" && id.type.shapeId === "BuiltInSetState";
|
||||
}
|
||||
|
||||
export function isUseReducerType(id: Identifier): boolean {
|
||||
return id.type.kind === "Function" && id.type.shapeId === "BuiltInUseReducer";
|
||||
}
|
||||
|
||||
export function isDispatcherType(id: Identifier): boolean {
|
||||
return id.type.kind === "Function" && id.type.shapeId === "BuiltInDispatch";
|
||||
}
|
||||
|
||||
export function isUseEffectHookType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === "Function" && id.type.shapeId === "BuiltInUseEffectHook"
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
SourceLocation,
|
||||
Terminal,
|
||||
VariableBinding,
|
||||
makeBlockId,
|
||||
@@ -174,7 +175,7 @@ export default class HIRBuilder {
|
||||
return handler ?? null;
|
||||
}
|
||||
|
||||
makeTemporary(): Identifier {
|
||||
makeTemporary(loc: SourceLocation): Identifier {
|
||||
const id = this.nextIdentifierId;
|
||||
return {
|
||||
id,
|
||||
@@ -182,6 +183,7 @@ export default class HIRBuilder {
|
||||
mutableRange: { start: makeInstructionId(0), end: makeInstructionId(0) },
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -320,6 +322,7 @@ export default class HIRBuilder {
|
||||
},
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
};
|
||||
this.#bindings.set(name, { node, identifier });
|
||||
return identifier;
|
||||
@@ -877,7 +880,10 @@ export function removeUnnecessaryTryCatch(fn: HIR): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function createTemporaryPlace(env: Environment): Place {
|
||||
export function createTemporaryPlace(
|
||||
env: Environment,
|
||||
loc: SourceLocation
|
||||
): Place {
|
||||
return {
|
||||
kind: "Identifier",
|
||||
identifier: {
|
||||
@@ -886,6 +892,7 @@ export function createTemporaryPlace(env: Environment): Place {
|
||||
name: null,
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc,
|
||||
},
|
||||
reactive: false,
|
||||
effect: Effect.Unknown,
|
||||
|
||||
@@ -118,6 +118,7 @@ function addShape(
|
||||
export type HookKind =
|
||||
| "useContext"
|
||||
| "useState"
|
||||
| "useReducer"
|
||||
| "useRef"
|
||||
| "useEffect"
|
||||
| "useLayoutEffect"
|
||||
@@ -200,6 +201,8 @@ export const BuiltInUseEffectHookId = "BuiltInUseEffectHook";
|
||||
export const BuiltInUseLayoutEffectHookId = "BuiltInUseLayoutEffectHook";
|
||||
export const BuiltInUseInsertionEffectHookId = "BuiltInUseInsertionEffectHook";
|
||||
export const BuiltInUseOperatorId = "BuiltInUseOperator";
|
||||
export const BuiltInUseReducerId = "BuiltInUseReducer";
|
||||
export const BuiltInDispatchId = "BuiltInDispatch";
|
||||
|
||||
// ShapeRegistry with default definitions for built-ins.
|
||||
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
|
||||
@@ -387,6 +390,25 @@ addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInUseReducerId, [
|
||||
["0", { kind: "Poly" }],
|
||||
[
|
||||
"1",
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
},
|
||||
BuiltInDispatchId
|
||||
),
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInUseRefId, [
|
||||
["current", { kind: "Object", shapeId: BuiltInRefValueId }],
|
||||
]);
|
||||
|
||||
+2
-2
@@ -178,7 +178,7 @@ function makeManualMemoizationMarkers(
|
||||
return [
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env),
|
||||
lvalue: createTemporaryPlace(env, fnExpr.loc),
|
||||
value: {
|
||||
kind: "StartMemoize",
|
||||
manualMemoId,
|
||||
@@ -193,7 +193,7 @@ function makeManualMemoizationMarkers(
|
||||
},
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env),
|
||||
lvalue: createTemporaryPlace(env, fnExpr.loc),
|
||||
value: {
|
||||
kind: "FinishMemoize",
|
||||
manualMemoId,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Place,
|
||||
computePostDominatorTree,
|
||||
getHookKind,
|
||||
isDispatcherType,
|
||||
isSetStateType,
|
||||
isUseOperator,
|
||||
} from "../HIR";
|
||||
@@ -219,7 +220,10 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
|
||||
if (hasReactiveInput) {
|
||||
for (const lvalue of eachInstructionLValue(instruction)) {
|
||||
if (isSetStateType(lvalue.identifier)) {
|
||||
if (
|
||||
isSetStateType(lvalue.identifier) ||
|
||||
isDispatcherType(lvalue.identifier)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
reactiveIdentifiers.markReactive(lvalue);
|
||||
|
||||
@@ -2117,6 +2117,8 @@ function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
return "Mutating component props or hook arguments is not allowed. Consider using a local variable instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
|
||||
} else {
|
||||
return "This mutates a variable that React considers immutable";
|
||||
}
|
||||
|
||||
+2
@@ -236,6 +236,7 @@ function rewriteBlock(
|
||||
name: null,
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc: terminal.loc,
|
||||
},
|
||||
kind: "Identifier",
|
||||
reactive: false,
|
||||
@@ -277,6 +278,7 @@ function declareTemporary(
|
||||
name: null,
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc: result.loc,
|
||||
},
|
||||
kind: "Identifier",
|
||||
reactive: false,
|
||||
|
||||
@@ -369,6 +369,58 @@ function evaluateInstruction(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "|": {
|
||||
if (typeof lhs === "number" && typeof rhs === "number") {
|
||||
result = { kind: "Primitive", value: lhs | rhs, loc: value.loc };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "&": {
|
||||
if (typeof lhs === "number" && typeof rhs === "number") {
|
||||
result = { kind: "Primitive", value: lhs & rhs, loc: value.loc };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "^": {
|
||||
if (typeof lhs === "number" && typeof rhs === "number") {
|
||||
result = { kind: "Primitive", value: lhs ^ rhs, loc: value.loc };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "<<": {
|
||||
if (typeof lhs === "number" && typeof rhs === "number") {
|
||||
result = { kind: "Primitive", value: lhs << rhs, loc: value.loc };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ">>": {
|
||||
if (typeof lhs === "number" && typeof rhs === "number") {
|
||||
result = { kind: "Primitive", value: lhs >> rhs, loc: value.loc };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ">>>": {
|
||||
if (typeof lhs === "number" && typeof rhs === "number") {
|
||||
result = {
|
||||
kind: "Primitive",
|
||||
value: lhs >>> rhs,
|
||||
loc: value.loc,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "%": {
|
||||
if (typeof lhs === "number" && typeof rhs === "number") {
|
||||
result = { kind: "Primitive", value: lhs % rhs, loc: value.loc };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "**": {
|
||||
if (typeof lhs === "number" && typeof rhs === "number") {
|
||||
result = { kind: "Primitive", value: lhs ** rhs, loc: value.loc };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "<": {
|
||||
if (typeof lhs === "number" && typeof rhs === "number") {
|
||||
result = { kind: "Primitive", value: lhs < rhs, loc: value.loc };
|
||||
|
||||
+145
-62
@@ -461,6 +461,11 @@ function codegenReactiveScope(
|
||||
): void {
|
||||
const cacheStoreStatements: Array<t.Statement> = [];
|
||||
const cacheLoadStatements: Array<t.Statement> = [];
|
||||
const cacheLoads: Array<{
|
||||
name: t.Identifier;
|
||||
index: number;
|
||||
value: t.Expression;
|
||||
}> = [];
|
||||
const changeExpressions: Array<t.Expression> = [];
|
||||
const changeExpressionComments: Array<string> = [];
|
||||
const outputComments: Array<string> = [];
|
||||
@@ -488,6 +493,10 @@ function codegenReactiveScope(
|
||||
} else {
|
||||
changeExpressions.push(comparison);
|
||||
}
|
||||
/*
|
||||
* Adding directly to cacheStoreStatements rather than cacheLoads, because there
|
||||
* is no corresponding cacheLoadStatement for dependencies
|
||||
*/
|
||||
cacheStoreStatements.push(
|
||||
t.expressionStatement(
|
||||
t.assignmentExpression(
|
||||
@@ -523,32 +532,7 @@ function codegenReactiveScope(
|
||||
t.variableDeclaration("let", [t.variableDeclarator(name)])
|
||||
);
|
||||
}
|
||||
cacheStoreStatements.push(
|
||||
t.expressionStatement(
|
||||
t.assignmentExpression(
|
||||
"=",
|
||||
t.memberExpression(
|
||||
t.identifier(cx.synthesizeName("$")),
|
||||
t.numericLiteral(index),
|
||||
true
|
||||
),
|
||||
wrapCacheDep(cx, name)
|
||||
)
|
||||
)
|
||||
);
|
||||
cacheLoadStatements.push(
|
||||
t.expressionStatement(
|
||||
t.assignmentExpression(
|
||||
"=",
|
||||
name,
|
||||
t.memberExpression(
|
||||
t.identifier(cx.synthesizeName("$")),
|
||||
t.numericLiteral(index),
|
||||
true
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
cacheLoads.push({ name, index, value: wrapCacheDep(cx, name) });
|
||||
cx.declare(identifier);
|
||||
}
|
||||
for (const reassignment of scope.reassignments) {
|
||||
@@ -558,34 +542,9 @@ function codegenReactiveScope(
|
||||
}
|
||||
const name = convertIdentifier(reassignment);
|
||||
outputComments.push(name.name);
|
||||
|
||||
cacheStoreStatements.push(
|
||||
t.expressionStatement(
|
||||
t.assignmentExpression(
|
||||
"=",
|
||||
t.memberExpression(
|
||||
t.identifier(cx.synthesizeName("$")),
|
||||
t.numericLiteral(index),
|
||||
true
|
||||
),
|
||||
wrapCacheDep(cx, name)
|
||||
)
|
||||
)
|
||||
);
|
||||
cacheLoadStatements.push(
|
||||
t.expressionStatement(
|
||||
t.assignmentExpression(
|
||||
"=",
|
||||
name,
|
||||
t.memberExpression(
|
||||
t.identifier(cx.synthesizeName("$")),
|
||||
t.numericLiteral(index),
|
||||
true
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
cacheLoads.push({ name, index, value: wrapCacheDep(cx, name) });
|
||||
}
|
||||
|
||||
let testCondition = (changeExpressions as Array<t.Expression>).reduce(
|
||||
(acc: t.Expression | null, ident: t.Expression) => {
|
||||
if (acc == null) {
|
||||
@@ -616,15 +575,139 @@ function codegenReactiveScope(
|
||||
);
|
||||
}
|
||||
|
||||
if (cx.env.config.disableMemoizationForDebugging) {
|
||||
CompilerError.invariant(
|
||||
cx.env.config.enableChangeDetectionForDebugging == null,
|
||||
{
|
||||
reason: `Expected to not have both change detection enabled and memoization disabled`,
|
||||
description: `Incompatible config options`,
|
||||
loc: null,
|
||||
}
|
||||
);
|
||||
testCondition = t.logicalExpression(
|
||||
"||",
|
||||
testCondition,
|
||||
t.booleanLiteral(true)
|
||||
);
|
||||
}
|
||||
let computationBlock = codegenBlock(cx, block);
|
||||
computationBlock.body.push(...cacheStoreStatements);
|
||||
const memoBlock = t.blockStatement(cacheLoadStatements);
|
||||
|
||||
const memoStatement = t.ifStatement(
|
||||
testCondition,
|
||||
computationBlock,
|
||||
memoBlock
|
||||
);
|
||||
let memoStatement;
|
||||
if (
|
||||
cx.env.config.enableChangeDetectionForDebugging != null &&
|
||||
changeExpressions.length > 0
|
||||
) {
|
||||
const loc =
|
||||
typeof scope.loc === "symbol"
|
||||
? "unknown location"
|
||||
: `(${scope.loc.start.line}:${scope.loc.end.line})`;
|
||||
const detectionFunction =
|
||||
cx.env.config.enableChangeDetectionForDebugging.importSpecifierName;
|
||||
const cacheLoadOldValueStatements: Array<t.Statement> = [];
|
||||
const changeDetectionStatements: Array<t.Statement> = [];
|
||||
const idempotenceDetectionStatements: Array<t.Statement> = [];
|
||||
|
||||
for (const { name, index, value } of cacheLoads) {
|
||||
const loadName = cx.synthesizeName(`old$${name.name}`);
|
||||
const slot = t.memberExpression(
|
||||
t.identifier(cx.synthesizeName("$")),
|
||||
t.numericLiteral(index),
|
||||
true
|
||||
);
|
||||
cacheStoreStatements.push(
|
||||
t.expressionStatement(t.assignmentExpression("=", slot, value))
|
||||
);
|
||||
cacheLoadOldValueStatements.push(
|
||||
t.variableDeclaration("let", [
|
||||
t.variableDeclarator(t.identifier(loadName), slot),
|
||||
])
|
||||
);
|
||||
changeDetectionStatements.push(
|
||||
t.expressionStatement(
|
||||
t.callExpression(t.identifier(detectionFunction), [
|
||||
t.identifier(loadName),
|
||||
name,
|
||||
t.stringLiteral(name.name),
|
||||
t.stringLiteral(cx.fnName),
|
||||
t.stringLiteral("cached"),
|
||||
t.stringLiteral(loc),
|
||||
])
|
||||
)
|
||||
);
|
||||
idempotenceDetectionStatements.push(
|
||||
t.expressionStatement(
|
||||
t.callExpression(t.identifier(detectionFunction), [
|
||||
slot,
|
||||
name,
|
||||
t.stringLiteral(name.name),
|
||||
t.stringLiteral(cx.fnName),
|
||||
t.stringLiteral("recomputed"),
|
||||
t.stringLiteral(loc),
|
||||
])
|
||||
)
|
||||
);
|
||||
idempotenceDetectionStatements.push(
|
||||
t.expressionStatement(t.assignmentExpression("=", name, slot))
|
||||
);
|
||||
}
|
||||
const condition = cx.synthesizeName("condition");
|
||||
memoStatement = t.blockStatement([
|
||||
...computationBlock.body,
|
||||
t.variableDeclaration("let", [
|
||||
t.variableDeclarator(t.identifier(condition), testCondition),
|
||||
]),
|
||||
t.ifStatement(
|
||||
t.unaryExpression("!", t.identifier(condition)),
|
||||
t.blockStatement([
|
||||
...cacheLoadOldValueStatements,
|
||||
...changeDetectionStatements,
|
||||
])
|
||||
),
|
||||
...cacheStoreStatements,
|
||||
t.ifStatement(
|
||||
t.identifier(condition),
|
||||
t.blockStatement([
|
||||
...computationBlock.body,
|
||||
...idempotenceDetectionStatements,
|
||||
])
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
for (const { name, index, value } of cacheLoads) {
|
||||
cacheStoreStatements.push(
|
||||
t.expressionStatement(
|
||||
t.assignmentExpression(
|
||||
"=",
|
||||
t.memberExpression(
|
||||
t.identifier(cx.synthesizeName("$")),
|
||||
t.numericLiteral(index),
|
||||
true
|
||||
),
|
||||
value
|
||||
)
|
||||
)
|
||||
);
|
||||
cacheLoadStatements.push(
|
||||
t.expressionStatement(
|
||||
t.assignmentExpression(
|
||||
"=",
|
||||
name,
|
||||
t.memberExpression(
|
||||
t.identifier(cx.synthesizeName("$")),
|
||||
t.numericLiteral(index),
|
||||
true
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
computationBlock.body.push(...cacheStoreStatements);
|
||||
memoStatement = t.ifStatement(
|
||||
testCondition,
|
||||
computationBlock,
|
||||
t.blockStatement(cacheLoadStatements)
|
||||
);
|
||||
}
|
||||
|
||||
if (cx.env.config.enableMemoizationComments) {
|
||||
if (changeExpressionComments.length) {
|
||||
@@ -665,9 +748,9 @@ function codegenReactiveScope(
|
||||
true
|
||||
);
|
||||
}
|
||||
if (memoBlock.body.length > 0) {
|
||||
if (cacheLoadStatements.length > 0) {
|
||||
t.addComment(
|
||||
memoBlock.body[0]!,
|
||||
cacheLoadStatements[0]!,
|
||||
"leading",
|
||||
` Inputs did not change, use cached value`,
|
||||
true
|
||||
|
||||
+22
-1
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { CompilerError } from "..";
|
||||
import { CompilerError, SourceLocation } from "..";
|
||||
import { Environment } from "../HIR";
|
||||
import {
|
||||
GeneratedSource,
|
||||
@@ -110,6 +110,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
reassignments: new Set(),
|
||||
earlyReturnValue: null,
|
||||
merged: new Set(),
|
||||
loc: identifier.loc,
|
||||
};
|
||||
scopes.set(groupIdentifier, scope);
|
||||
} else {
|
||||
@@ -119,6 +120,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
scope.range.end = makeInstructionId(
|
||||
Math.max(scope.range.end, identifier.mutableRange.end)
|
||||
);
|
||||
scope.loc = mergeLocation(scope.loc, identifier.loc);
|
||||
}
|
||||
identifier.scope = scope;
|
||||
identifier.mutableRange = scope.range;
|
||||
@@ -159,6 +161,25 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function mergeLocation(l: SourceLocation, r: SourceLocation): SourceLocation {
|
||||
if (l === GeneratedSource) {
|
||||
return r;
|
||||
} else if (r === GeneratedSource) {
|
||||
return l;
|
||||
} else {
|
||||
return {
|
||||
start: {
|
||||
line: Math.min(l.start.line, r.start.line),
|
||||
column: Math.min(l.start.column, r.start.column),
|
||||
},
|
||||
end: {
|
||||
line: Math.max(l.end.line, r.end.line),
|
||||
column: Math.max(l.end.column, r.end.column),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Is the operand mutable at this given instruction
|
||||
export function isMutable({ id }: Instruction, place: Place): boolean {
|
||||
const range = place.identifier.mutableRange;
|
||||
|
||||
+5
-5
@@ -153,10 +153,10 @@ class Transform extends ReactiveFunctionTransform<State> {
|
||||
|
||||
const instructions = scopeBlock.instructions;
|
||||
const loc = earlyReturnValue.loc;
|
||||
const sentinelTemp = createTemporaryPlace(this.env);
|
||||
const symbolTemp = createTemporaryPlace(this.env);
|
||||
const forTemp = createTemporaryPlace(this.env);
|
||||
const argTemp = createTemporaryPlace(this.env);
|
||||
const sentinelTemp = createTemporaryPlace(this.env, loc);
|
||||
const symbolTemp = createTemporaryPlace(this.env, loc);
|
||||
const forTemp = createTemporaryPlace(this.env, loc);
|
||||
const argTemp = createTemporaryPlace(this.env, loc);
|
||||
scopeBlock.instructions = [
|
||||
{
|
||||
kind: "instruction",
|
||||
@@ -274,7 +274,7 @@ class Transform extends ReactiveFunctionTransform<State> {
|
||||
if (state.earlyReturnValue !== null) {
|
||||
earlyReturnValue = state.earlyReturnValue;
|
||||
} else {
|
||||
const identifier = createTemporaryPlace(this.env).identifier;
|
||||
const identifier = createTemporaryPlace(this.env, loc).identifier;
|
||||
promoteTemporary(identifier);
|
||||
earlyReturnValue = {
|
||||
label: this.env.nextBlockId,
|
||||
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { CompilerError } from "../CompilerError";
|
||||
import {
|
||||
Environment,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
Place,
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveTerminalStatement,
|
||||
getHookKind,
|
||||
isUseRefType,
|
||||
isUseStateType,
|
||||
} from "../HIR";
|
||||
import { eachCallArgument, eachInstructionLValue } from "../HIR/visitors";
|
||||
import DisjointSet from "../Utils/DisjointSet";
|
||||
import { assertExhaustive } from "../Utils/utils";
|
||||
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
|
||||
|
||||
/**
|
||||
* This pass is built based on the observation by @jbrown215 that arguments
|
||||
* to useState and useRef are only used the first time a component is rendered.
|
||||
* Any subsequent times, the arguments will be evaluated but ignored. In this pass,
|
||||
* we use this fact to improve the output of the compiler by not recomputing values that
|
||||
* are only used as arguments (or inputs to arguments to) useState and useRef.
|
||||
*
|
||||
* This pass isn't yet stress-tested so it's not enabled by default. It's only enabled
|
||||
* to support certain debug modes that detect non-idempotent code, since non-idempotent
|
||||
* code can "safely" be used if its only passed to useState and useRef. We plan to rewrite
|
||||
* this pass in HIR and enable it as an optimization in the future.
|
||||
*
|
||||
* Algorithm:
|
||||
* We take two passes over the reactive function AST. In the first pass, we gather
|
||||
* aliases and build relationships between property accesses--the key thing we need
|
||||
* to do here is to find that, e.g., $0.x and $1 refer to the same value if
|
||||
* $1 = PropertyLoad $0.x.
|
||||
*
|
||||
* In the second pass, we traverse the AST in reverse order and track how each place
|
||||
* is used. If a place is read from in any Terminal, we mark the place as "Update", meaning
|
||||
* it is used whenever the component is updated/re-rendered. If a place is read from in
|
||||
* a useState or useRef hook call, we mark it as "Create", since it is only used when the
|
||||
* component is created. In other instructions, we propagate the inferred place for the
|
||||
* instructions lvalues onto any other instructions that are read.
|
||||
*
|
||||
* Whenever we finish this reverse pass over a reactive block, we can look at the blocks
|
||||
* dependencies and see whether the dependencies are used in an "Update" context or only
|
||||
* in a "Create" context. If a dependency is create-only, then we can remove that dependency
|
||||
* from the block.
|
||||
*/
|
||||
|
||||
type CreateUpdate = "Create" | "Update" | "Unknown";
|
||||
|
||||
type KindMap = Map<IdentifierId, CreateUpdate>;
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
|
||||
map: KindMap = new Map();
|
||||
aliases: DisjointSet<IdentifierId>;
|
||||
paths: Map<IdentifierId, Map<string, IdentifierId>>;
|
||||
env: Environment;
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
aliases: DisjointSet<IdentifierId>,
|
||||
paths: Map<IdentifierId, Map<string, IdentifierId>>
|
||||
) {
|
||||
super();
|
||||
this.aliases = aliases;
|
||||
this.paths = paths;
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
join(values: Array<CreateUpdate>): CreateUpdate {
|
||||
function join2(l: CreateUpdate, r: CreateUpdate): CreateUpdate {
|
||||
if (l === "Update" || r === "Update") {
|
||||
return "Update";
|
||||
} else if (l === "Create" || r === "Create") {
|
||||
return "Create";
|
||||
} else if (l === "Unknown" || r === "Unknown") {
|
||||
return "Unknown";
|
||||
}
|
||||
assertExhaustive(r, `Unhandled variable kind ${r}`);
|
||||
}
|
||||
return values.reduce(join2, "Unknown");
|
||||
}
|
||||
|
||||
isCreateOnlyHook(id: Identifier): boolean {
|
||||
return isUseStateType(id) || isUseRefType(id);
|
||||
}
|
||||
|
||||
override visitPlace(
|
||||
_: InstructionId,
|
||||
place: Place,
|
||||
state: CreateUpdate
|
||||
): void {
|
||||
this.map.set(
|
||||
place.identifier.id,
|
||||
this.join([state, this.map.get(place.identifier.id) ?? "Unknown"])
|
||||
);
|
||||
}
|
||||
|
||||
override visitBlock(block: ReactiveBlock, state: CreateUpdate): void {
|
||||
super.visitBlock([...block].reverse(), state);
|
||||
}
|
||||
|
||||
override visitInstruction(instruction: ReactiveInstruction): void {
|
||||
const state = this.join(
|
||||
[...eachInstructionLValue(instruction)].map(
|
||||
(operand) => this.map.get(operand.identifier.id) ?? "Unknown"
|
||||
)
|
||||
);
|
||||
|
||||
const visitCallOrMethodNonArgs = (): void => {
|
||||
switch (instruction.value.kind) {
|
||||
case "CallExpression": {
|
||||
this.visitPlace(instruction.id, instruction.value.callee, state);
|
||||
break;
|
||||
}
|
||||
case "MethodCall": {
|
||||
this.visitPlace(instruction.id, instruction.value.property, state);
|
||||
this.visitPlace(instruction.id, instruction.value.receiver, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isHook = (): boolean => {
|
||||
let callee = null;
|
||||
switch (instruction.value.kind) {
|
||||
case "CallExpression": {
|
||||
callee = instruction.value.callee.identifier;
|
||||
break;
|
||||
}
|
||||
case "MethodCall": {
|
||||
callee = instruction.value.property.identifier;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return callee != null && getHookKind(this.env, callee) != null;
|
||||
};
|
||||
|
||||
switch (instruction.value.kind) {
|
||||
case "CallExpression":
|
||||
case "MethodCall": {
|
||||
if (
|
||||
instruction.lvalue &&
|
||||
this.isCreateOnlyHook(instruction.lvalue.identifier)
|
||||
) {
|
||||
[...eachCallArgument(instruction.value.args)].forEach((operand) =>
|
||||
this.visitPlace(instruction.id, operand, "Create")
|
||||
);
|
||||
visitCallOrMethodNonArgs();
|
||||
} else {
|
||||
this.traverseInstruction(instruction, isHook() ? "Update" : state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.traverseInstruction(instruction, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override visitScope(scope: ReactiveScopeBlock): void {
|
||||
const state = this.join(
|
||||
[
|
||||
...scope.scope.declarations.keys(),
|
||||
...[...scope.scope.reassignments.values()].map((ident) => ident.id),
|
||||
].map((id) => this.map.get(id) ?? "Unknown")
|
||||
);
|
||||
super.visitScope(scope, state);
|
||||
[...scope.scope.dependencies].forEach((ident) => {
|
||||
let target: undefined | IdentifierId =
|
||||
this.aliases.find(ident.identifier.id) ?? ident.identifier.id;
|
||||
ident.path.forEach((key) => {
|
||||
target &&= this.paths.get(target)?.get(key);
|
||||
});
|
||||
if (target && this.map.get(target) === "Create") {
|
||||
scope.scope.dependencies.delete(ident);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override visitTerminal(
|
||||
stmt: ReactiveTerminalStatement,
|
||||
state: CreateUpdate
|
||||
): void {
|
||||
CompilerError.invariant(state !== "Create", {
|
||||
reason: "Visiting a terminal statement with state 'Create'",
|
||||
loc: stmt.terminal.loc,
|
||||
});
|
||||
super.visitTerminal(stmt, state);
|
||||
}
|
||||
|
||||
override visitReactiveFunctionValue(
|
||||
_id: InstructionId,
|
||||
_dependencies: Array<Place>,
|
||||
fn: ReactiveFunction,
|
||||
state: CreateUpdate
|
||||
): void {
|
||||
visitReactiveFunction(fn, this, state);
|
||||
}
|
||||
}
|
||||
|
||||
export default function pruneInitializationDependencies(
|
||||
fn: ReactiveFunction
|
||||
): void {
|
||||
const [aliases, paths] = getAliases(fn);
|
||||
visitReactiveFunction(fn, new Visitor(fn.env, aliases, paths), "Update");
|
||||
}
|
||||
|
||||
function update(
|
||||
map: Map<IdentifierId, Map<string, IdentifierId>>,
|
||||
key: IdentifierId,
|
||||
path: string,
|
||||
value: IdentifierId
|
||||
): void {
|
||||
const inner = map.get(key) ?? new Map();
|
||||
inner.set(path, value);
|
||||
map.set(key, inner);
|
||||
}
|
||||
|
||||
class AliasVisitor extends ReactiveFunctionVisitor {
|
||||
scopeIdentifiers: DisjointSet<IdentifierId> = new DisjointSet<IdentifierId>();
|
||||
scopePaths: Map<IdentifierId, Map<string, IdentifierId>> = new Map();
|
||||
|
||||
override visitInstruction(instr: ReactiveInstruction): void {
|
||||
if (
|
||||
instr.value.kind === "StoreLocal" ||
|
||||
instr.value.kind === "StoreContext"
|
||||
) {
|
||||
this.scopeIdentifiers.union([
|
||||
instr.value.lvalue.place.identifier.id,
|
||||
instr.value.value.identifier.id,
|
||||
]);
|
||||
} else if (
|
||||
instr.value.kind === "LoadLocal" ||
|
||||
instr.value.kind === "LoadContext"
|
||||
) {
|
||||
instr.lvalue &&
|
||||
this.scopeIdentifiers.union([
|
||||
instr.lvalue.identifier.id,
|
||||
instr.value.place.identifier.id,
|
||||
]);
|
||||
} else if (instr.value.kind === "PropertyLoad") {
|
||||
instr.lvalue &&
|
||||
update(
|
||||
this.scopePaths,
|
||||
instr.value.object.identifier.id,
|
||||
instr.value.property,
|
||||
instr.lvalue.identifier.id
|
||||
);
|
||||
} else if (instr.value.kind === "PropertyStore") {
|
||||
update(
|
||||
this.scopePaths,
|
||||
instr.value.object.identifier.id,
|
||||
instr.value.property,
|
||||
instr.value.value.identifier.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAliases(
|
||||
fn: ReactiveFunction
|
||||
): [DisjointSet<IdentifierId>, Map<IdentifierId, Map<string, IdentifierId>>] {
|
||||
const visitor = new AliasVisitor();
|
||||
visitReactiveFunction(fn, visitor, null);
|
||||
let disjoint = visitor.scopeIdentifiers;
|
||||
let scopePaths = new Map<IdentifierId, Map<string, IdentifierId>>();
|
||||
for (const [key, value] of visitor.scopePaths) {
|
||||
for (const [path, id] of value) {
|
||||
update(
|
||||
scopePaths,
|
||||
disjoint.find(key) ?? key,
|
||||
path,
|
||||
disjoint.find(id) ?? id
|
||||
);
|
||||
}
|
||||
}
|
||||
return [disjoint, scopePaths];
|
||||
}
|
||||
+5
-1
@@ -10,6 +10,7 @@ import {
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScopeBlock,
|
||||
isDispatcherType,
|
||||
isSetStateType,
|
||||
} from "../HIR";
|
||||
import { eachPatternOperand } from "../HIR/visitors";
|
||||
@@ -56,7 +57,10 @@ class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
|
||||
case "Destructure": {
|
||||
if (state.has(value.value.identifier.id)) {
|
||||
for (const lvalue of eachPatternOperand(value.lvalue.pattern)) {
|
||||
if (isSetStateType(lvalue.identifier)) {
|
||||
if (
|
||||
isSetStateType(lvalue.identifier) ||
|
||||
isDispatcherType(lvalue.identifier)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
state.add(lvalue.identifier.id);
|
||||
|
||||
@@ -86,6 +86,7 @@ class SSABuilder {
|
||||
},
|
||||
scope: null, // reset along w the mutable range
|
||||
type: makeType(),
|
||||
loc: oldId.loc,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -22,10 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
function f() {
|
||||
let x;
|
||||
|
||||
x = 3 >>> 1;
|
||||
return x;
|
||||
return 1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableChangeDetectionForDebugging
|
||||
function Component(props) {
|
||||
let x = null;
|
||||
if (props.cond) {
|
||||
x = [];
|
||||
x.push(props.value);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { $structuralCheck } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableChangeDetectionForDebugging
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let x = null;
|
||||
if (props.cond) {
|
||||
{
|
||||
x = [];
|
||||
x.push(props.value);
|
||||
let condition = $[0] !== props.value;
|
||||
if (!condition) {
|
||||
let old$x = $[1];
|
||||
$structuralCheck(old$x, x, "x", "Component", "cached", "(3:6)");
|
||||
}
|
||||
$[0] = props.value;
|
||||
$[1] = x;
|
||||
if (condition) {
|
||||
x = [];
|
||||
x.push(props.value);
|
||||
$structuralCheck($[1], x, "x", "Component", "recomputed", "(3:6)");
|
||||
x = $[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// @enableChangeDetectionForDebugging
|
||||
function Component(props) {
|
||||
let x = null;
|
||||
if (props.cond) {
|
||||
x = [];
|
||||
x.push(props.value);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function foo() {
|
||||
return (
|
||||
<Stringify
|
||||
value={[
|
||||
123.45 | 0,
|
||||
123.45 & 0,
|
||||
123.45 ^ 0,
|
||||
123 << 0,
|
||||
123 >> 0,
|
||||
123 >>> 0,
|
||||
123.45 | 1,
|
||||
123.45 & 1,
|
||||
123.45 ^ 1,
|
||||
123 << 1,
|
||||
123 >> 1,
|
||||
123 >>> 1,
|
||||
3 ** 2,
|
||||
3 ** 2.5,
|
||||
3.5 ** 2,
|
||||
2 ** (3 ** 0.5),
|
||||
4 % 2,
|
||||
4 % 2.5,
|
||||
4 % 3,
|
||||
4.5 % 2,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function foo() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (
|
||||
<Stringify
|
||||
value={[
|
||||
123, 0, 123, 123, 123, 123, 123, 1, 122, 246, 61, 61, 9,
|
||||
15.588457268119896, 12.25, 3.3219970854839125, 0, 1.5, 1, 0.5,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"value":[123,0,123,123,123,123,123,1,122,246,61,61,9,15.588457268119896,12.25,3.3219970854839125,0,1.5,1,0.5]}</div>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function foo() {
|
||||
return (
|
||||
<Stringify
|
||||
value={[
|
||||
123.45 | 0,
|
||||
123.45 & 0,
|
||||
123.45 ^ 0,
|
||||
123 << 0,
|
||||
123 >> 0,
|
||||
123 >>> 0,
|
||||
123.45 | 1,
|
||||
123.45 & 1,
|
||||
123.45 ^ 1,
|
||||
123 << 1,
|
||||
123 >> 1,
|
||||
123 >>> 1,
|
||||
3 ** 2,
|
||||
3 ** 2.5,
|
||||
3.5 ** 2,
|
||||
2 ** (3 ** 0.5),
|
||||
4 % 2,
|
||||
4 % 2.5,
|
||||
4 % 3,
|
||||
4.5 % 2,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import { useReducer } from "react";
|
||||
|
||||
function Foo() {
|
||||
let [state, setState] = useReducer({ foo: 1 });
|
||||
state.foo = 1;
|
||||
return state;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
3 | function Foo() {
|
||||
4 | let [state, setState] = useReducer({ foo: 1 });
|
||||
> 5 | state.foo = 1;
|
||||
| ^^^^^ InvalidReact: Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead (5:5)
|
||||
6 | return state;
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { useReducer } from "react";
|
||||
|
||||
function Foo() {
|
||||
let [state, setState] = useReducer({ foo: 1 });
|
||||
state.foo = 1;
|
||||
return state;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @disableMemoizationForDebugging @enableChangeDetectionForDebugging
|
||||
function Component(props) {}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
InvalidConfig: Invalid environment config: the 'disableMemoizationForDebugging' and 'enableChangeDetectionForDebugging' options cannot be used together
|
||||
```
|
||||
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// @disableMemoizationForDebugging @enableChangeDetectionForDebugging
|
||||
function Component(props) {}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @disableMemoizationForDebugging
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Component({ a }) {
|
||||
let x = useMemo(() => [a], []);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @disableMemoizationForDebugging
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { a } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || true) {
|
||||
t1 = () => [a];
|
||||
$[0] = a;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel") || true) {
|
||||
t2 = [];
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const x = useMemo(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== x || true) {
|
||||
t3 = <div>{x}</div>;
|
||||
$[3] = x;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>42</div>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// @disableMemoizationForDebugging
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Component({ a }) {
|
||||
let x = useMemo(() => [a], []);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingManualUseMemo
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Component({ a }) {
|
||||
let x = useMemo(() => [a], []);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingManualUseMemo
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { a } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
t1 = () => [a];
|
||||
$[0] = a;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const x = useMemo(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== x) {
|
||||
t3 = <div>{x}</div>;
|
||||
$[3] = x;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>42</div>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// @enablePreserveExistingManualUseMemo
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Component({ a }) {
|
||||
let x = useMemo(() => [a], []);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import { useReducer } from "react";
|
||||
|
||||
function f() {
|
||||
const [state, dispatch] = useReducer();
|
||||
|
||||
const onClick = () => {
|
||||
dispatch();
|
||||
};
|
||||
|
||||
return <div onClick={onClick} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: f,
|
||||
params: [],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useReducer } from "react";
|
||||
|
||||
function f() {
|
||||
const $ = _c(1);
|
||||
const [state, dispatch] = useReducer();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const onClick = () => {
|
||||
dispatch();
|
||||
};
|
||||
|
||||
t0 = <div onClick={onClick} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: f,
|
||||
params: [],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div></div>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { useReducer } from "react";
|
||||
|
||||
function f() {
|
||||
const [state, dispatch] = useReducer();
|
||||
|
||||
const onClick = () => {
|
||||
dispatch();
|
||||
};
|
||||
|
||||
return <div onClick={onClick} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: f,
|
||||
params: [],
|
||||
isComponent: true,
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import { useState } from "react"; // @enableChangeDetectionForDebugging
|
||||
|
||||
function useOther(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
function Component(props) {
|
||||
const w = f(props.x);
|
||||
const z = useOther(w);
|
||||
const [x, _] = useState(z);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
function f(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { $structuralCheck } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useState } from "react"; // @enableChangeDetectionForDebugging
|
||||
|
||||
function useOther(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
{
|
||||
t0 = f(props.x);
|
||||
let condition = $[0] !== props.x;
|
||||
if (!condition) {
|
||||
let old$t0 = $[1];
|
||||
$structuralCheck(old$t0, t0, "t0", "Component", "cached", "(8:8)");
|
||||
}
|
||||
$[0] = props.x;
|
||||
$[1] = t0;
|
||||
if (condition) {
|
||||
t0 = f(props.x);
|
||||
$structuralCheck($[1], t0, "t0", "Component", "recomputed", "(8:8)");
|
||||
t0 = $[1];
|
||||
}
|
||||
}
|
||||
const w = t0;
|
||||
const z = useOther(w);
|
||||
const [x] = useState(z);
|
||||
let t1;
|
||||
{
|
||||
t1 = <div>{x}</div>;
|
||||
let condition = $[2] !== x;
|
||||
if (!condition) {
|
||||
let old$t1 = $[3];
|
||||
$structuralCheck(old$t1, t1, "t1", "Component", "cached", "(11:11)");
|
||||
}
|
||||
$[2] = x;
|
||||
$[3] = t1;
|
||||
if (condition) {
|
||||
t1 = <div>{x}</div>;
|
||||
$structuralCheck($[3], t1, "t1", "Component", "recomputed", "(11:11)");
|
||||
t1 = $[3];
|
||||
}
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
function f(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { useState } from "react"; // @enableChangeDetectionForDebugging
|
||||
|
||||
function useOther(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
function Component(props) {
|
||||
const w = f(props.x);
|
||||
const z = useOther(w);
|
||||
const [x, _] = useState(z);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
function f(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableChangeDetectionForDebugging
|
||||
import { useState } from "react";
|
||||
|
||||
function Component(props) {
|
||||
const [x, _] = useState(f(props.x));
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { $structuralCheck } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableChangeDetectionForDebugging
|
||||
import { useState } from "react";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = f(props.x);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const [x] = useState(t0);
|
||||
let t1;
|
||||
{
|
||||
t1 = <div>{x}</div>;
|
||||
let condition = $[1] !== x;
|
||||
if (!condition) {
|
||||
let old$t1 = $[2];
|
||||
$structuralCheck(old$t1, t1, "t1", "Component", "cached", "(6:6)");
|
||||
}
|
||||
$[1] = x;
|
||||
$[2] = t1;
|
||||
if (condition) {
|
||||
t1 = <div>{x}</div>;
|
||||
$structuralCheck($[2], t1, "t1", "Component", "recomputed", "(6:6)");
|
||||
t1 = $[2];
|
||||
}
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// @enableChangeDetectionForDebugging
|
||||
import { useState } from "react";
|
||||
|
||||
function Component(props) {
|
||||
const [x, _] = useState(f(props.x));
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import { useState } from "react"; // @enableChangeDetectionForDebugging
|
||||
|
||||
function Component(props) {
|
||||
const w = f(props.x);
|
||||
const [x, _] = useState(w);
|
||||
return (
|
||||
<div>
|
||||
{x}
|
||||
{w}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function f(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { $structuralCheck } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useState } from "react"; // @enableChangeDetectionForDebugging
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
{
|
||||
t0 = f(props.x);
|
||||
let condition = $[0] !== props.x;
|
||||
if (!condition) {
|
||||
let old$t0 = $[1];
|
||||
$structuralCheck(old$t0, t0, "t0", "Component", "cached", "(4:4)");
|
||||
}
|
||||
$[0] = props.x;
|
||||
$[1] = t0;
|
||||
if (condition) {
|
||||
t0 = f(props.x);
|
||||
$structuralCheck($[1], t0, "t0", "Component", "recomputed", "(4:4)");
|
||||
t0 = $[1];
|
||||
}
|
||||
}
|
||||
const w = t0;
|
||||
const [x] = useState(w);
|
||||
let t1;
|
||||
{
|
||||
t1 = (
|
||||
<div>
|
||||
{x}
|
||||
{w}
|
||||
</div>
|
||||
);
|
||||
let condition = $[2] !== x || $[3] !== w;
|
||||
if (!condition) {
|
||||
let old$t1 = $[4];
|
||||
$structuralCheck(old$t1, t1, "t1", "Component", "cached", "(7:10)");
|
||||
}
|
||||
$[2] = x;
|
||||
$[3] = w;
|
||||
$[4] = t1;
|
||||
if (condition) {
|
||||
t1 = (
|
||||
<div>
|
||||
{x}
|
||||
{w}
|
||||
</div>
|
||||
);
|
||||
$structuralCheck($[4], t1, "t1", "Component", "recomputed", "(7:10)");
|
||||
t1 = $[4];
|
||||
}
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
function f(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { useState } from "react"; // @enableChangeDetectionForDebugging
|
||||
|
||||
function Component(props) {
|
||||
const w = f(props.x);
|
||||
const [x, _] = useState(w);
|
||||
return (
|
||||
<div>
|
||||
{x}
|
||||
{w}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function f(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 42 }],
|
||||
isComponent: true,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-plugin-react-compiler",
|
||||
"version": "0.0.0-experimental-a97cca1-20240529",
|
||||
"version": "0.0.0-experimental-51a85ea-20240601",
|
||||
"description": "ESLint plugin to display errors found by the React compiler.",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -124,12 +124,14 @@ const rule: Rule.RuleModule = {
|
||||
});
|
||||
} catch {}
|
||||
} else {
|
||||
babelAST = HermesParser.parse(sourceCode, {
|
||||
babel: true,
|
||||
enableExperimentalComponentSyntax: true,
|
||||
sourceFilename: filename,
|
||||
sourceType: "module",
|
||||
});
|
||||
try {
|
||||
babelAST = HermesParser.parse(sourceCode, {
|
||||
babel: true,
|
||||
enableExperimentalComponentSyntax: true,
|
||||
sourceFilename: filename,
|
||||
sourceType: "module",
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (babelAST != null) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-compiler-healthcheck",
|
||||
"version": "0.0.0-experimental-31393f7-20240529",
|
||||
"version": "0.0.0-experimental-7054a14-20240601",
|
||||
"description": "Health check script to test violations of the rules of react.",
|
||||
"bin": {
|
||||
"react-compiler-healthcheck": "dist/index.js"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const config = {
|
||||
knownIncompatibleLibraries: ["mobx"],
|
||||
knownIncompatibleLibraries: ["mobx", "@risingstack/react-easy-state"],
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
const { useRef, useEffect } = React;
|
||||
const { useRef, useEffect, isValidElement } = React;
|
||||
const ReactSecretInternals =
|
||||
//@ts-ignore
|
||||
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ??
|
||||
@@ -251,3 +251,172 @@ export function useRenderCounter(name: string): void {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const seenErrors = new Set();
|
||||
|
||||
export function $structuralCheck(
|
||||
oldValue: any,
|
||||
newValue: any,
|
||||
variableName: string,
|
||||
fnName: string,
|
||||
kind: string,
|
||||
loc: string
|
||||
): void {
|
||||
function error(l: string, r: string, path: string, depth: number) {
|
||||
const str = `${fnName}:${loc} [${kind}] ${variableName}${path} changed from ${l} to ${r} at depth ${depth}`;
|
||||
if (seenErrors.has(str)) {
|
||||
return;
|
||||
}
|
||||
seenErrors.add(str);
|
||||
console.error(str);
|
||||
}
|
||||
const depthLimit = 2;
|
||||
function recur(oldValue: any, newValue: any, path: string, depth: number) {
|
||||
if (depth > depthLimit) {
|
||||
return;
|
||||
} else if (oldValue === newValue) {
|
||||
return;
|
||||
} else if (typeof oldValue !== typeof newValue) {
|
||||
error(`type ${typeof oldValue}`, `type ${typeof newValue}`, path, depth);
|
||||
} else if (typeof oldValue === "object") {
|
||||
const oldArray = Array.isArray(oldValue);
|
||||
const newArray = Array.isArray(newValue);
|
||||
if (oldValue === null && newValue !== null) {
|
||||
error("null", `type ${typeof newValue}`, path, depth);
|
||||
} else if (newValue === null) {
|
||||
error(`type ${typeof oldValue}`, "null", path, depth);
|
||||
} else if (oldValue instanceof Map) {
|
||||
if (!(newValue instanceof Map)) {
|
||||
error(`Map instance`, `other value`, path, depth);
|
||||
} else if (oldValue.size !== newValue.size) {
|
||||
error(
|
||||
`Map instance with size ${oldValue.size}`,
|
||||
`Map instance with size ${newValue.size}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
} else {
|
||||
for (const [k, v] of oldValue) {
|
||||
if (!newValue.has(k)) {
|
||||
error(
|
||||
`Map instance with key ${k}`,
|
||||
`Map instance without key ${k}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
} else {
|
||||
recur(v, newValue.get(k), `${path}.get(${k})`, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (newValue instanceof Map) {
|
||||
error("other value", `Map instance`, path, depth);
|
||||
} else if (oldValue instanceof Set) {
|
||||
if (!(newValue instanceof Set)) {
|
||||
error(`Set instance`, `other value`, path, depth);
|
||||
} else if (oldValue.size !== newValue.size) {
|
||||
error(
|
||||
`Set instance with size ${oldValue.size}`,
|
||||
`Set instance with size ${newValue.size}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
} else {
|
||||
for (const v of newValue) {
|
||||
if (!oldValue.has(v)) {
|
||||
error(
|
||||
`Set instance without element ${v}`,
|
||||
`Set instance with element ${v}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (newValue instanceof Set) {
|
||||
error("other value", `Set instance`, path, depth);
|
||||
} else if (oldArray || newArray) {
|
||||
if (oldArray !== newArray) {
|
||||
error(
|
||||
`type ${oldArray ? "array" : "object"}`,
|
||||
`type ${newArray ? "array" : "object"}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
} else if (oldValue.length !== newValue.length) {
|
||||
error(
|
||||
`array with length ${oldValue.length}`,
|
||||
`array with length ${newValue.length}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
} else {
|
||||
for (let ii = 0; ii < oldValue.length; ii++) {
|
||||
recur(oldValue[ii], newValue[ii], `${path}[${ii}]`, depth + 1);
|
||||
}
|
||||
}
|
||||
} else if (isValidElement(oldValue) || isValidElement(newValue)) {
|
||||
if (isValidElement(oldValue) !== isValidElement(newValue)) {
|
||||
error(
|
||||
`type ${isValidElement(oldValue) ? "React element" : "object"}`,
|
||||
`type ${isValidElement(newValue) ? "React element" : "object"}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
} else if (oldValue.type !== newValue.type) {
|
||||
error(
|
||||
`React element of type ${oldValue.type}`,
|
||||
`React element of type ${newValue.type}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
} else {
|
||||
recur(
|
||||
oldValue.props,
|
||||
newValue.props,
|
||||
`[props of ${path}]`,
|
||||
depth + 1
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const key in newValue) {
|
||||
if (!(key in oldValue)) {
|
||||
error(
|
||||
`object without key ${key}`,
|
||||
`object with key ${key}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const key in oldValue) {
|
||||
if (!(key in newValue)) {
|
||||
error(
|
||||
`object with key ${key}`,
|
||||
`object without key ${key}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
} else {
|
||||
recur(oldValue[key], newValue[key], `${path}.${key}`, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof oldValue === "function") {
|
||||
// Bail on functions for now
|
||||
return;
|
||||
} else if (isNaN(oldValue) || isNaN(newValue)) {
|
||||
if (isNaN(oldValue) !== isNaN(newValue)) {
|
||||
error(
|
||||
`${isNaN(oldValue) ? "NaN" : "non-NaN value"}`,
|
||||
`${isNaN(newValue) ? "NaN" : "non-NaN value"}`,
|
||||
path,
|
||||
depth
|
||||
);
|
||||
}
|
||||
} else if (oldValue !== newValue) {
|
||||
error(oldValue, newValue, path, depth);
|
||||
}
|
||||
}
|
||||
recur(oldValue, newValue, "", 0);
|
||||
}
|
||||
|
||||
@@ -495,6 +495,10 @@ const skipFilter = new Set([
|
||||
"flag-enable-emit-hook-guards",
|
||||
|
||||
"fast-refresh-refresh-on-const-changes-dev",
|
||||
"useState-pruned-dependency-change-detect",
|
||||
"useState-unpruned-dependency",
|
||||
"useState-and-other-hook-unpruned-dependency",
|
||||
"change-detect-reassign",
|
||||
]);
|
||||
|
||||
export default skipFilter;
|
||||
|
||||
@@ -43,6 +43,7 @@ function makePluginOptions(
|
||||
let hookPattern: string | null = null;
|
||||
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
|
||||
let validatePreserveExistingMemoizationGuarantees = false;
|
||||
let enableChangeDetectionForDebugging = null;
|
||||
|
||||
if (firstLine.indexOf("@compilationMode(annotation)") !== -1) {
|
||||
assert(
|
||||
@@ -120,6 +121,12 @@ function makePluginOptions(
|
||||
validatePreserveExistingMemoizationGuarantees = true;
|
||||
}
|
||||
|
||||
if (firstLine.includes("@enableChangeDetectionForDebugging")) {
|
||||
enableChangeDetectionForDebugging = {
|
||||
source: "react-compiler-runtime",
|
||||
importSpecifierName: "$structuralCheck",
|
||||
};
|
||||
}
|
||||
const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
|
||||
if (
|
||||
hookPatternMatch &&
|
||||
@@ -173,6 +180,7 @@ function makePluginOptions(
|
||||
enableSharedRuntime__testonly: true,
|
||||
hookPattern,
|
||||
validatePreserveExistingMemoizationGuarantees,
|
||||
enableChangeDetectionForDebugging,
|
||||
},
|
||||
compilationMode,
|
||||
logger: null,
|
||||
@@ -183,6 +191,7 @@ function makePluginOptions(
|
||||
eslintSuppressionRules,
|
||||
flowSuppressions,
|
||||
ignoreUseNoForget,
|
||||
enableReanimatedCheck: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -153,8 +153,8 @@ function subscribeFilterFile(
|
||||
} else if (
|
||||
events.findIndex((event) => event.path.includes(FILTER_FILENAME)) !== -1
|
||||
) {
|
||||
state.filter = await readTestFilter();
|
||||
if (state.mode.filter) {
|
||||
state.filter = await readTestFilter();
|
||||
state.mode.action = RunnerAction.Test;
|
||||
onChange(state);
|
||||
}
|
||||
@@ -189,7 +189,7 @@ function subscribeKeyEvents(
|
||||
state: RunnerState,
|
||||
onChange: (state: RunnerState) => void
|
||||
) {
|
||||
process.stdin.on("keypress", (str, key) => {
|
||||
process.stdin.on("keypress", async (str, key) => {
|
||||
if (key.name === "u") {
|
||||
// u => update fixtures
|
||||
state.mode.action = RunnerAction.Update;
|
||||
@@ -197,6 +197,7 @@ function subscribeKeyEvents(
|
||||
process.exit(0);
|
||||
} else if (key.name === "f") {
|
||||
state.mode.filter = !state.mode.filter;
|
||||
state.filter = state.mode.filter ? await readTestFilter() : null;
|
||||
state.mode.action = RunnerAction.Test;
|
||||
} else {
|
||||
// any other key re-runs tests
|
||||
@@ -218,7 +219,7 @@ export async function makeWatchRunner(
|
||||
action: RunnerAction.Test,
|
||||
filter: filterMode,
|
||||
},
|
||||
filter: await readTestFilter(),
|
||||
filter: filterMode ? await readTestFilter() : null,
|
||||
};
|
||||
|
||||
subscribeTsc(state, onChange);
|
||||
|
||||
@@ -199,7 +199,7 @@ module.exports = function (webpackEnv) {
|
||||
? shouldUseSourceMap
|
||||
? 'source-map'
|
||||
: false
|
||||
: isEnvDevelopment && 'cheap-module-source-map',
|
||||
: isEnvDevelopment && 'source-map',
|
||||
// These are the "entry points" to our application.
|
||||
// This means they will be the "root" imports that are included in JS bundle.
|
||||
entry: isEnvProduction
|
||||
|
||||
@@ -16,6 +16,7 @@ const babelOptions = {
|
||||
'@babel/plugin-syntax-import-meta',
|
||||
'@babel/plugin-transform-react-jsx',
|
||||
],
|
||||
sourceMaps: process.env.NODE_ENV === 'development' ? 'inline' : false,
|
||||
};
|
||||
|
||||
async function babelLoad(url, context, defaultLoad) {
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"",
|
||||
"dev:global": "NODE_ENV=development BUILD_PATH=dist node --experimental-loader ./loader/global.js server/global",
|
||||
"dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --experimental-loader ./loader/region.js --conditions=react-server server/region",
|
||||
"dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server server/region",
|
||||
"start": "node scripts/build.js && concurrently \"npm run start:region\" \"npm run start:global\"",
|
||||
"start:global": "NODE_ENV=production node --experimental-loader ./loader/global.js server/global",
|
||||
"start:region": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region",
|
||||
|
||||
@@ -214,6 +214,43 @@ app.all('/', async function (req, res, next) {
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(express.static('public'));
|
||||
|
||||
app.get('/source-maps', async function (req, res, next) {
|
||||
// Proxy the request to the regional server.
|
||||
const proxiedHeaders = {
|
||||
'X-Forwarded-Host': req.hostname,
|
||||
'X-Forwarded-For': req.ips,
|
||||
'X-Forwarded-Port': 3000,
|
||||
'X-Forwarded-Proto': req.protocol,
|
||||
};
|
||||
|
||||
const promiseForData = request(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port: 3001,
|
||||
method: req.method,
|
||||
path: req.originalUrl,
|
||||
headers: proxiedHeaders,
|
||||
},
|
||||
req
|
||||
);
|
||||
|
||||
try {
|
||||
const rscResponse = await promiseForData;
|
||||
res.set('Content-type', 'application/json');
|
||||
rscResponse.on('data', data => {
|
||||
res.write(data);
|
||||
res.flush();
|
||||
});
|
||||
rscResponse.on('end', data => {
|
||||
res.end();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Failed to proxy request: ${e.stack}`);
|
||||
res.statusCode = 500;
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// In production we host the static build output.
|
||||
app.use(express.static('build'));
|
||||
|
||||
@@ -24,6 +24,7 @@ babelRegister({
|
||||
],
|
||||
presets: ['@babel/preset-react'],
|
||||
plugins: ['@babel/transform-modules-commonjs'],
|
||||
sourceMaps: process.env.NODE_ENV === 'development' ? 'inline' : false,
|
||||
});
|
||||
|
||||
if (typeof fetch === 'undefined') {
|
||||
@@ -38,6 +39,8 @@ const app = express();
|
||||
const compress = require('compression');
|
||||
const {Readable} = require('node:stream');
|
||||
|
||||
const nodeModule = require('node:module');
|
||||
|
||||
app.use(compress());
|
||||
|
||||
// Application
|
||||
@@ -176,6 +179,71 @@ app.get('/todos', function (req, res) {
|
||||
]);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const rootDir = path.resolve(__dirname, '../');
|
||||
|
||||
app.get('/source-maps', async function (req, res, next) {
|
||||
try {
|
||||
res.set('Content-type', 'application/json');
|
||||
let requestedFilePath = req.query.name;
|
||||
|
||||
let isCompiledOutput = false;
|
||||
if (requestedFilePath.startsWith('file://')) {
|
||||
// We assume that if it was prefixed with file:// it's referring to the compiled output
|
||||
// and if it's a direct file path we assume it's source mapped back to original format.
|
||||
isCompiledOutput = true;
|
||||
requestedFilePath = requestedFilePath.slice(7);
|
||||
}
|
||||
|
||||
const relativePath = path.relative(rootDir, requestedFilePath);
|
||||
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
||||
// This is outside the root directory of the app. Forbid it to be served.
|
||||
res.status = 403;
|
||||
res.write('{}');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceMap = nodeModule.findSourceMap(requestedFilePath);
|
||||
let map;
|
||||
// There are two ways to return a source map depending on what we observe in error.stack.
|
||||
// A real app will have a similar choice to make for which strategy to pick.
|
||||
if (!sourceMap || !isCompiledOutput) {
|
||||
// If a file doesn't have a source map, such as this file, then we generate a blank
|
||||
// source map that just contains the original content and segments pointing to the
|
||||
// original lines.
|
||||
// Similarly
|
||||
const sourceContent = await readFile(requestedFilePath, 'utf8');
|
||||
const lines = sourceContent.split('\n').length;
|
||||
map = {
|
||||
version: 3,
|
||||
sources: [requestedFilePath],
|
||||
sourcesContent: [sourceContent],
|
||||
// Note: This approach to mapping each line only lets you jump to each line
|
||||
// not jump to a column within a line. To do that, you need a proper source map
|
||||
// generated for each parsed segment or add a segment for each column.
|
||||
mappings: 'AAAA' + ';AACA'.repeat(lines - 1),
|
||||
sourceRoot: '',
|
||||
};
|
||||
} else {
|
||||
// We always set prepareStackTrace before reading the stack so that we get the stack
|
||||
// without source maps applied. Therefore we have to use the original source map.
|
||||
// If something read .stack before we did, we might observe the line/column after
|
||||
// source mapping back to the original file. We use the isCompiledOutput check above
|
||||
// in that case.
|
||||
map = sourceMap.payload;
|
||||
}
|
||||
res.write(JSON.stringify(map));
|
||||
res.end();
|
||||
} catch (x) {
|
||||
res.status = 500;
|
||||
res.write('{}');
|
||||
res.end();
|
||||
console.error(x);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(3001, () => {
|
||||
console.log('Regional Flight Server listening on port 3001...');
|
||||
});
|
||||
|
||||
@@ -39,6 +39,9 @@ async function hydrateApp() {
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
findSourceMapURL(fileName) {
|
||||
return '/source-maps?name=' + encodeURIComponent(fileName);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+257
-5
@@ -67,8 +67,11 @@ import {
|
||||
REACT_ELEMENT_TYPE,
|
||||
REACT_POSTPONE_TYPE,
|
||||
ASYNC_ITERATOR,
|
||||
REACT_FRAGMENT_TYPE,
|
||||
} from 'shared/ReactSymbols';
|
||||
|
||||
import getComponentNameFromType from 'shared/getComponentNameFromType';
|
||||
|
||||
export type {CallServerCallback, EncodeFormActionCallback};
|
||||
|
||||
interface FlightStreamController {
|
||||
@@ -236,6 +239,8 @@ Chunk.prototype.then = function <T>(
|
||||
}
|
||||
};
|
||||
|
||||
export type FindSourceMapURLCallback = (fileName: string) => null | string;
|
||||
|
||||
export type Response = {
|
||||
_bundlerConfig: SSRModuleMap,
|
||||
_moduleLoading: ModuleLoading,
|
||||
@@ -251,6 +256,8 @@ export type Response = {
|
||||
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
|
||||
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
|
||||
_tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
|
||||
_debugRootTask?: null | ConsoleTask, // DEV-only
|
||||
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
|
||||
};
|
||||
|
||||
function readChunk<T>(chunk: SomeChunk<T>): T {
|
||||
@@ -573,7 +580,45 @@ function nullRefGetter() {
|
||||
}
|
||||
}
|
||||
|
||||
function getServerComponentTaskName(componentInfo: ReactComponentInfo): string {
|
||||
return '<' + (componentInfo.name || '...') + '>';
|
||||
}
|
||||
|
||||
function getTaskName(type: mixed): string {
|
||||
if (type === REACT_FRAGMENT_TYPE) {
|
||||
return '<>';
|
||||
}
|
||||
if (typeof type === 'function') {
|
||||
// This is a function so it must have been a Client Reference that resolved to
|
||||
// a function. We use "use client" to indicate that this is the boundary into
|
||||
// the client. There should only be one for any given owner chain.
|
||||
return '"use client"';
|
||||
}
|
||||
if (
|
||||
typeof type === 'object' &&
|
||||
type !== null &&
|
||||
type.$$typeof === REACT_LAZY_TYPE
|
||||
) {
|
||||
if (type._init === readChunk) {
|
||||
// This is a lazy node created by Flight. It is probably a client reference.
|
||||
// We use the "use client" string to indicate that this is the boundary into
|
||||
// the client. There will only be one for any given owner chain.
|
||||
return '"use client"';
|
||||
}
|
||||
// We don't want to eagerly initialize the initializer in DEV mode so we can't
|
||||
// call it to extract the type so we don't know the type of this component.
|
||||
return '<...>';
|
||||
}
|
||||
try {
|
||||
const name = getComponentNameFromType(type);
|
||||
return name ? '<' + name + '>' : '<...>';
|
||||
} catch (x) {
|
||||
return '<...>';
|
||||
}
|
||||
}
|
||||
|
||||
function createElement(
|
||||
response: Response,
|
||||
type: mixed,
|
||||
key: mixed,
|
||||
props: mixed,
|
||||
@@ -647,11 +692,34 @@ function createElement(
|
||||
writable: true,
|
||||
value: stack,
|
||||
});
|
||||
|
||||
let task: null | ConsoleTask = null;
|
||||
if (supportsCreateTask && stack !== null) {
|
||||
const createTaskFn = (console: any).createTask.bind(
|
||||
console,
|
||||
getTaskName(type),
|
||||
);
|
||||
const callStack = buildFakeCallStack(response, stack, createTaskFn);
|
||||
// This owner should ideally have already been initialized to avoid getting
|
||||
// user stack frames on the stack.
|
||||
const ownerTask =
|
||||
owner === null ? null : initializeFakeTask(response, owner);
|
||||
if (ownerTask === null) {
|
||||
const rootTask = response._debugRootTask;
|
||||
if (rootTask != null) {
|
||||
task = rootTask.run(callStack);
|
||||
} else {
|
||||
task = callStack();
|
||||
}
|
||||
} else {
|
||||
task = ownerTask.run(callStack);
|
||||
}
|
||||
}
|
||||
Object.defineProperty(element, '_debugTask', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: null,
|
||||
value: task,
|
||||
});
|
||||
}
|
||||
// TODO: We should be freezing the element but currently, we might write into
|
||||
@@ -1049,6 +1117,7 @@ function parseModelTuple(
|
||||
// TODO: Consider having React just directly accept these arrays as elements.
|
||||
// Or even change the ReactElement type to be an array.
|
||||
return createElement(
|
||||
response,
|
||||
tuple[1],
|
||||
tuple[2],
|
||||
tuple[3],
|
||||
@@ -1074,6 +1143,7 @@ export function createResponse(
|
||||
encodeFormAction: void | EncodeFormActionCallback,
|
||||
nonce: void | string,
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback,
|
||||
): Response {
|
||||
const chunks: Map<number, SomeChunk<any>> = new Map();
|
||||
const response: Response = {
|
||||
@@ -1092,6 +1162,17 @@ export function createResponse(
|
||||
_buffer: [],
|
||||
_tempRefs: temporaryReferences,
|
||||
};
|
||||
if (supportsCreateTask) {
|
||||
// Any stacks that appear on the server need to be rooted somehow on the client
|
||||
// so we create a root Task for this response which will be the root owner for any
|
||||
// elements created by the server. We use the "use server" string to indicate that
|
||||
// this is where we enter the server from the client.
|
||||
// TODO: Make this string configurable.
|
||||
response._debugRootTask = (console: any).createTask('"use server"');
|
||||
}
|
||||
if (__DEV__) {
|
||||
response._debugFindSourceMapURL = findSourceMapURL;
|
||||
}
|
||||
// Don't inline this call because it causes closure to outline the call above.
|
||||
response._fromJSON = createFromJSONCallback(response);
|
||||
return response;
|
||||
@@ -1582,6 +1663,151 @@ function resolveHint<Code: HintCode>(
|
||||
dispatchHint(code, hintModel);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-internal/no-production-logging
|
||||
const supportsCreateTask =
|
||||
__DEV__ && enableOwnerStacks && !!(console: any).createTask;
|
||||
|
||||
const taskCache: null | WeakMap<
|
||||
ReactComponentInfo | ReactAsyncInfo,
|
||||
ConsoleTask,
|
||||
> = supportsCreateTask ? new WeakMap() : null;
|
||||
|
||||
type FakeFunction<T> = (() => T) => T;
|
||||
const fakeFunctionCache: Map<string, FakeFunction<any>> = __DEV__
|
||||
? new Map()
|
||||
: (null: any);
|
||||
|
||||
function createFakeFunction<T>(
|
||||
name: string,
|
||||
filename: string,
|
||||
sourceMap: null | string,
|
||||
line: number,
|
||||
col: number,
|
||||
): FakeFunction<T> {
|
||||
// This creates a fake copy of a Server Module. It represents a module that has already
|
||||
// executed on the server but we re-execute a blank copy for its stack frames on the client.
|
||||
|
||||
const comment =
|
||||
'/* This module was rendered by a Server Component. Turn on Source Maps to see the server source. */';
|
||||
|
||||
// We generate code where the call is at the line and column of the server executed code.
|
||||
// This allows us to use the original source map as the source map of this fake file to
|
||||
// point to the original source.
|
||||
let code;
|
||||
if (line <= 1) {
|
||||
code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment + '\n';
|
||||
} else {
|
||||
code =
|
||||
comment +
|
||||
'\n'.repeat(line - 2) +
|
||||
'_=>\n' +
|
||||
' '.repeat(col < 1 ? 0 : col - 1) +
|
||||
'_()\n';
|
||||
}
|
||||
|
||||
if (sourceMap) {
|
||||
code += '//# sourceMappingURL=' + sourceMap;
|
||||
} else if (filename) {
|
||||
code += '//# sourceURL=' + filename;
|
||||
}
|
||||
|
||||
let fn: FakeFunction<T>;
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
fn = (0, eval)(code);
|
||||
} catch (x) {
|
||||
// If eval fails, such as if in an environment that doesn't support it,
|
||||
// we fallback to creating a function here. It'll still have the right
|
||||
// name but it'll lose line/column number and file name.
|
||||
fn = function (_) {
|
||||
return _();
|
||||
};
|
||||
}
|
||||
// $FlowFixMe[cannot-write]
|
||||
Object.defineProperty(fn, 'name', {value: name || '(anonymous)'});
|
||||
// $FlowFixMe[prop-missing]
|
||||
fn.displayName = name;
|
||||
return fn;
|
||||
}
|
||||
|
||||
// This matches either of these V8 formats.
|
||||
// at name (filename:0:0)
|
||||
// at filename:0:0
|
||||
// at async filename:0:0
|
||||
const frameRegExp =
|
||||
/^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/;
|
||||
|
||||
function buildFakeCallStack<T>(
|
||||
response: Response,
|
||||
stack: string,
|
||||
innerCall: () => T,
|
||||
): () => T {
|
||||
const frames = stack.split('\n');
|
||||
let callStack = innerCall;
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
const frame = frames[i];
|
||||
let fn = fakeFunctionCache.get(frame);
|
||||
if (fn === undefined) {
|
||||
const parsed = frameRegExp.exec(frame);
|
||||
if (!parsed) {
|
||||
// We assume the server returns a V8 compatible stack trace.
|
||||
continue;
|
||||
}
|
||||
const name = parsed[1] || '';
|
||||
const filename = parsed[2] || parsed[5] || '';
|
||||
const line = +(parsed[3] || parsed[6]);
|
||||
const col = +(parsed[4] || parsed[7]);
|
||||
const sourceMap = response._debugFindSourceMapURL
|
||||
? response._debugFindSourceMapURL(filename)
|
||||
: null;
|
||||
fn = createFakeFunction(name, filename, sourceMap, line, col);
|
||||
// TODO: This cache should technically live on the response since the _debugFindSourceMapURL
|
||||
// function is an input and can vary by response.
|
||||
fakeFunctionCache.set(frame, fn);
|
||||
}
|
||||
callStack = fn.bind(null, callStack);
|
||||
}
|
||||
return callStack;
|
||||
}
|
||||
|
||||
function initializeFakeTask(
|
||||
response: Response,
|
||||
debugInfo: ReactComponentInfo | ReactAsyncInfo,
|
||||
): null | ConsoleTask {
|
||||
if (taskCache === null || typeof debugInfo.stack !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const componentInfo: ReactComponentInfo = (debugInfo: any); // Refined
|
||||
const stack: string = debugInfo.stack;
|
||||
const cachedEntry = taskCache.get((componentInfo: any));
|
||||
if (cachedEntry !== undefined) {
|
||||
return cachedEntry;
|
||||
}
|
||||
|
||||
const ownerTask =
|
||||
componentInfo.owner == null
|
||||
? null
|
||||
: initializeFakeTask(response, componentInfo.owner);
|
||||
|
||||
// eslint-disable-next-line react-internal/no-production-logging
|
||||
const createTaskFn = (console: any).createTask.bind(
|
||||
console,
|
||||
getServerComponentTaskName(componentInfo),
|
||||
);
|
||||
const callStack = buildFakeCallStack(response, stack, createTaskFn);
|
||||
|
||||
if (ownerTask === null) {
|
||||
const rootTask = response._debugRootTask;
|
||||
if (rootTask != null) {
|
||||
return rootTask.run(callStack);
|
||||
} else {
|
||||
return callStack();
|
||||
}
|
||||
} else {
|
||||
return ownerTask.run(callStack);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDebugInfo(
|
||||
response: Response,
|
||||
id: number,
|
||||
@@ -1594,6 +1820,10 @@ function resolveDebugInfo(
|
||||
'resolveDebugInfo should never be called in production mode. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
// We eagerly initialize the fake task because this resolving happens outside any
|
||||
// render phase so we're not inside a user space stack at this point. If we waited
|
||||
// to initialize it when we need it, we might be inside user code.
|
||||
initializeFakeTask(response, debugInfo);
|
||||
const chunk = getChunk(response, id);
|
||||
const chunkDebugInfo: ReactDebugInfo =
|
||||
chunk._debugInfo || (chunk._debugInfo = []);
|
||||
@@ -1615,12 +1845,34 @@ function resolveConsoleEntry(
|
||||
const payload: [string, string, null | ReactComponentInfo, string, mixed] =
|
||||
parseModel(response, value);
|
||||
const methodName = payload[0];
|
||||
// TODO: Restore the fake stack before logging.
|
||||
// const stackTrace = payload[1];
|
||||
// const owner = payload[2];
|
||||
const stackTrace = payload[1];
|
||||
const owner = payload[2];
|
||||
const env = payload[3];
|
||||
const args = payload.slice(4);
|
||||
printToConsole(methodName, args, env);
|
||||
if (!enableOwnerStacks) {
|
||||
// Printing with stack isn't really limited to owner stacks but
|
||||
// we gate it behind the same flag for now while iterating.
|
||||
printToConsole(methodName, args, env);
|
||||
return;
|
||||
}
|
||||
const callStack = buildFakeCallStack(
|
||||
response,
|
||||
stackTrace,
|
||||
printToConsole.bind(null, methodName, args, env),
|
||||
);
|
||||
if (owner != null) {
|
||||
const task = initializeFakeTask(response, owner);
|
||||
if (task !== null) {
|
||||
task.run(callStack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const rootTask = response._debugRootTask;
|
||||
if (rootTask != null) {
|
||||
rootTask.run(callStack);
|
||||
return;
|
||||
}
|
||||
callStack();
|
||||
}
|
||||
|
||||
function mergeBuffer(
|
||||
|
||||
+18
-10
@@ -868,7 +868,12 @@ function findCommonAncestorIndex(rootStack: any, hookStack: any) {
|
||||
}
|
||||
|
||||
function isReactWrapper(functionName: any, wrapperName: string) {
|
||||
return parseHookName(functionName) === wrapperName;
|
||||
const hookName = parseHookName(functionName);
|
||||
if (wrapperName === 'HostTransitionStatus') {
|
||||
return hookName === wrapperName || hookName === 'FormStatus';
|
||||
}
|
||||
|
||||
return hookName === wrapperName;
|
||||
}
|
||||
|
||||
function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
|
||||
@@ -878,21 +883,24 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
|
||||
return -1;
|
||||
}
|
||||
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
|
||||
// Note: there is no guarantee that we will find the top-most primitive frame in the stack
|
||||
// For React Native (uses Hermes), these source fields will be identical and skipped
|
||||
if (primitiveStack[i].source !== hookStack[i].source) {
|
||||
// If the next frame is a method from the dispatcher, we
|
||||
// assume that the next frame after that is the actual public API call.
|
||||
// This prohibits nesting dispatcher calls in hooks.
|
||||
// If the next two frames are functions called `useX` then we assume that they're part of the
|
||||
// wrappers that the React package or other packages adds around the dispatcher.
|
||||
if (
|
||||
i < hookStack.length - 1 &&
|
||||
isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
|
||||
) {
|
||||
i++;
|
||||
// Guard against the dispatcher call being inlined.
|
||||
// At this point we wouldn't be able to recover the actual React Hook name.
|
||||
if (i < hookStack.length - 1) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (
|
||||
i < hookStack.length - 1 &&
|
||||
isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -1040,7 +1048,7 @@ function buildTree(
|
||||
const levelChild: HooksNode = {
|
||||
id,
|
||||
isStateEditable,
|
||||
name: name,
|
||||
name,
|
||||
value: hook.value,
|
||||
subHooks: [],
|
||||
debugInfo: debugInfo,
|
||||
|
||||
@@ -279,7 +279,6 @@ function initialize(socket: WebSocket) {
|
||||
// $FlowFixMe[incompatible-call] found when upgrading Flow
|
||||
store = new Store(bridge, {
|
||||
checkBridgeProtocolCompatibility: true,
|
||||
supportsNativeInspection: true,
|
||||
supportsTraceUpdates: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ function createBridgeAndStore() {
|
||||
// At this time, the timeline can only parse Chrome performance profiles.
|
||||
supportsTimeline: __IS_CHROME__,
|
||||
supportsTraceUpdates: true,
|
||||
supportsNativeInspection: true,
|
||||
});
|
||||
|
||||
if (!isProfiling) {
|
||||
|
||||
@@ -37,7 +37,6 @@ export function createStore(bridge: FrontendBridge, config?: Config): Store {
|
||||
return new Store(bridge, {
|
||||
checkBridgeProtocolCompatibility: true,
|
||||
supportsTraceUpdates: true,
|
||||
supportsNativeInspection: true,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export function createStore(bridge: FrontendBridge, config?: Config): Store {
|
||||
checkBridgeProtocolCompatibility: true,
|
||||
supportsTraceUpdates: true,
|
||||
supportsTimeline: true,
|
||||
supportsNativeInspection: true,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
+31
-2
@@ -1915,8 +1915,12 @@ describe('Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
it('from react get counted', () => {
|
||||
// In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088
|
||||
// When the error is emitted, the source fiber of this error is not yet mounted
|
||||
// So DevTools can't connect the error and the fiber
|
||||
// TODO(hoxyq): update RDT to keep track of such fibers
|
||||
// @reactVersion >= 19.0
|
||||
it('from react get counted [React >= 19]', () => {
|
||||
function Example() {
|
||||
return [<Child />];
|
||||
}
|
||||
@@ -1938,6 +1942,31 @@ describe('Store', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.0
|
||||
it('from react get counted [React 18.x]', () => {
|
||||
function Example() {
|
||||
return [<Child />];
|
||||
}
|
||||
function Child() {
|
||||
return null;
|
||||
}
|
||||
|
||||
withErrorsOrWarningsIgnored(
|
||||
['Warning: Each child in a list should have a unique "key" prop'],
|
||||
() => {
|
||||
act(() => render(<Example />));
|
||||
},
|
||||
);
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 1, ⚠ 0
|
||||
[root]
|
||||
▾ <Example> ✕
|
||||
<Child>
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
it('can be cleared for the whole app', () => {
|
||||
function Example() {
|
||||
|
||||
+4
-2
@@ -172,7 +172,7 @@ export default class Store extends EventEmitter<{
|
||||
_rootIDToRendererID: Map<number, number> = new Map();
|
||||
|
||||
// These options may be initially set by a configuration option when constructing the Store.
|
||||
_supportsNativeInspection: boolean = true;
|
||||
_supportsNativeInspection: boolean = false;
|
||||
_supportsReloadAndProfile: boolean = false;
|
||||
_supportsTimeline: boolean = false;
|
||||
_supportsTraceUpdates: boolean = false;
|
||||
@@ -216,7 +216,9 @@ export default class Store extends EventEmitter<{
|
||||
supportsTimeline,
|
||||
supportsTraceUpdates,
|
||||
} = config;
|
||||
this._supportsNativeInspection = supportsNativeInspection !== false;
|
||||
if (supportsNativeInspection) {
|
||||
this._supportsNativeInspection = true;
|
||||
}
|
||||
if (supportsReloadAndProfile) {
|
||||
this._supportsReloadAndProfile = true;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"dependencies": {
|
||||
"cross-spawn": "^5.0.1",
|
||||
"electron": "^23.1.2",
|
||||
"ip": "^1.1.4",
|
||||
"internal-ip": "^6.2.0",
|
||||
"minimist": "^1.2.3",
|
||||
"react-devtools-core": "5.2.0",
|
||||
"update-notifier": "^2.1.0"
|
||||
|
||||
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
||||
const {clipboard, shell, contextBridge} = require('electron');
|
||||
const fs = require('fs');
|
||||
const {address} = require('ip');
|
||||
const internalIP = require('internal-ip');
|
||||
|
||||
// Expose protected methods so that render process does not need unsafe node integration
|
||||
contextBridge.exposeInMainWorld('api', {
|
||||
electron: {clipboard, shell},
|
||||
ip: {address},
|
||||
ip: {address: internalIP.v4.sync},
|
||||
getDevTools() {
|
||||
let devtools;
|
||||
try {
|
||||
|
||||
@@ -2358,6 +2358,7 @@ export function getResource(
|
||||
type: string,
|
||||
currentProps: any,
|
||||
pendingProps: any,
|
||||
currentResource: null | Resource,
|
||||
): null | Resource {
|
||||
const resourceRoot = getCurrentResourceRoot();
|
||||
if (!resourceRoot) {
|
||||
@@ -2430,9 +2431,44 @@ export function getResource(
|
||||
);
|
||||
}
|
||||
}
|
||||
if (currentProps && currentResource === null) {
|
||||
// This node was previously an Instance type and is becoming a Resource type
|
||||
// For now we error because we don't support flavor changes
|
||||
let diff = '';
|
||||
if (__DEV__) {
|
||||
diff = `
|
||||
|
||||
- ${describeLinkForResourceErrorDEV(currentProps)}
|
||||
+ ${describeLinkForResourceErrorDEV(pendingProps)}`;
|
||||
}
|
||||
throw new Error(
|
||||
'Expected <link> not to update to be updated to a stylesheet with precedence.' +
|
||||
' Check the `rel`, `href`, and `precedence` props of this component.' +
|
||||
' Alternatively, check whether two different <link> components render in the same slot or share the same key.' +
|
||||
diff,
|
||||
);
|
||||
}
|
||||
return resource;
|
||||
} else {
|
||||
if (currentProps && currentResource !== null) {
|
||||
// This node was previously a Resource type and is becoming an Instance type
|
||||
// For now we error because we don't support flavor changes
|
||||
let diff = '';
|
||||
if (__DEV__) {
|
||||
diff = `
|
||||
|
||||
- ${describeLinkForResourceErrorDEV(currentProps)}
|
||||
+ ${describeLinkForResourceErrorDEV(pendingProps)}`;
|
||||
}
|
||||
throw new Error(
|
||||
'Expected stylesheet with precedence to not be updated to a different kind of <link>.' +
|
||||
' Check the `rel`, `href`, and `precedence` props of this component.' +
|
||||
' Alternatively, check whether two different <link> components render in the same slot or share the same key.' +
|
||||
diff,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'script': {
|
||||
const async = pendingProps.async;
|
||||
@@ -2473,6 +2509,49 @@ export function getResource(
|
||||
}
|
||||
}
|
||||
|
||||
function describeLinkForResourceErrorDEV(props: any) {
|
||||
if (__DEV__) {
|
||||
let describedProps = 0;
|
||||
|
||||
let description = '<link';
|
||||
if (typeof props.rel === 'string') {
|
||||
describedProps++;
|
||||
description += ` rel="${props.rel}"`;
|
||||
} else if (hasOwnProperty.call(props, 'rel')) {
|
||||
describedProps++;
|
||||
description += ` rel="${
|
||||
props.rel === null ? 'null' : 'invalid type ' + typeof props.rel
|
||||
}"`;
|
||||
}
|
||||
if (typeof props.href === 'string') {
|
||||
describedProps++;
|
||||
description += ` href="${props.href}"`;
|
||||
} else if (hasOwnProperty.call(props, 'href')) {
|
||||
describedProps++;
|
||||
description += ` href="${
|
||||
props.href === null ? 'null' : 'invalid type ' + typeof props.href
|
||||
}"`;
|
||||
}
|
||||
if (typeof props.precedence === 'string') {
|
||||
describedProps++;
|
||||
description += ` precedence="${props.precedence}"`;
|
||||
} else if (hasOwnProperty.call(props, 'precedence')) {
|
||||
describedProps++;
|
||||
description += ` precedence={${
|
||||
props.precedence === null
|
||||
? 'null'
|
||||
: 'invalid type ' + typeof props.precedence
|
||||
}}`;
|
||||
}
|
||||
if (Object.getOwnPropertyNames(props).length > describedProps) {
|
||||
description += ' ...';
|
||||
}
|
||||
description += ' />';
|
||||
return description;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function styleTagPropsFromRawProps(
|
||||
rawProps: StyleTagQualifyingProps,
|
||||
): StyleTagProps {
|
||||
|
||||
@@ -20,6 +20,14 @@ export function scheduleWork(callback: () => void) {
|
||||
callback();
|
||||
}
|
||||
|
||||
export function scheduleMicrotask(callback: () => void) {
|
||||
// While this defies the method name the legacy builds have special
|
||||
// overrides that make work scheduling sync. At the moment scheduleMicrotask
|
||||
// isn't used by any legacy APIs so this is somewhat academic but if they
|
||||
// did in the future we'd probably want to have this be in sync with scheduleWork
|
||||
callback();
|
||||
}
|
||||
|
||||
export function flushBuffered(destination: Destination) {}
|
||||
|
||||
export function beginWriting(destination: Destination) {}
|
||||
|
||||
+22
-4
@@ -10,6 +10,7 @@
|
||||
'use strict';
|
||||
|
||||
import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils';
|
||||
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
@@ -21,12 +22,16 @@ let ReactDOMServer;
|
||||
let Scheduler;
|
||||
let assertLog;
|
||||
let container;
|
||||
let act;
|
||||
|
||||
describe('ReactClassComponentPropResolutionFizz', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
Scheduler = require('scheduler');
|
||||
patchMessageChannel(Scheduler);
|
||||
act = require('internal-test-utils').act;
|
||||
|
||||
React = require('react');
|
||||
ReactDOMServer = require('react-dom/server.browser');
|
||||
assertLog = require('internal-test-utils').assertLog;
|
||||
container = document.createElement('div');
|
||||
@@ -37,6 +42,17 @@ describe('ReactClassComponentPropResolutionFizz', () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
async function serverAct(callback) {
|
||||
let maybePromise;
|
||||
await act(() => {
|
||||
maybePromise = callback();
|
||||
if (maybePromise && typeof maybePromise.catch === 'function') {
|
||||
maybePromise.catch(() => {});
|
||||
}
|
||||
});
|
||||
return maybePromise;
|
||||
}
|
||||
|
||||
async function readIntoContainer(stream) {
|
||||
const reader = stream.getReader();
|
||||
let result = '';
|
||||
@@ -57,7 +73,7 @@ describe('ReactClassComponentPropResolutionFizz', () => {
|
||||
return text;
|
||||
}
|
||||
|
||||
test('resolves ref and default props before calling lifecycle methods', async () => {
|
||||
it('resolves ref and default props before calling lifecycle methods', async () => {
|
||||
function getPropKeys(props) {
|
||||
return Object.keys(props).join(', ');
|
||||
}
|
||||
@@ -80,11 +96,13 @@ describe('ReactClassComponentPropResolutionFizz', () => {
|
||||
};
|
||||
|
||||
// `ref` should never appear as a prop. `default` always should.
|
||||
|
||||
const ref = React.createRef();
|
||||
const stream = await ReactDOMServer.renderToReadableStream(
|
||||
<Component text="Yay" ref={ref} />,
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<Component text="Yay" ref={ref} />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
|
||||
assertLog([
|
||||
'constructor: text, default',
|
||||
'componentWillMount: text, default',
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
insertNodesAndExecuteScripts,
|
||||
getVisibleChildren,
|
||||
} from '../test-utils/FizzTestUtils';
|
||||
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
@@ -33,13 +34,14 @@ let Suspense;
|
||||
describe('ReactDOMFizzForm', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
Scheduler = require('scheduler');
|
||||
patchMessageChannel(Scheduler);
|
||||
act = require('internal-test-utils').act;
|
||||
React = require('react');
|
||||
ReactDOMServer = require('react-dom/server.browser');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
useDeferredValue = React.useDeferredValue;
|
||||
Suspense = React.Suspense;
|
||||
act = require('internal-test-utils').act;
|
||||
assertLog = require('internal-test-utils').assertLog;
|
||||
waitForPaint = require('internal-test-utils').waitForPaint;
|
||||
container = document.createElement('div');
|
||||
@@ -50,6 +52,17 @@ describe('ReactDOMFizzForm', () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
async function serverAct(callback) {
|
||||
let maybePromise;
|
||||
await act(() => {
|
||||
maybePromise = callback();
|
||||
if (maybePromise && typeof maybePromise.catch === 'function') {
|
||||
maybePromise.catch(() => {});
|
||||
}
|
||||
});
|
||||
return maybePromise;
|
||||
}
|
||||
|
||||
async function readIntoContainer(stream) {
|
||||
const reader = stream.getReader();
|
||||
let result = '';
|
||||
@@ -76,7 +89,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
return useDeferredValue('Final', 'Initial');
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
expect(container.textContent).toEqual('Initial');
|
||||
|
||||
@@ -107,7 +122,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
expect(container.textContent).toEqual('Loading...');
|
||||
|
||||
@@ -153,8 +170,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
|
||||
const cRef = React.createRef();
|
||||
|
||||
// The server renders using the "initial" value for B.
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
assertLog(['A', 'B [Initial]', 'C']);
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
|
||||
+57
-15
@@ -10,6 +10,7 @@
|
||||
'use strict';
|
||||
|
||||
import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils';
|
||||
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
@@ -24,10 +25,13 @@ let ReactDOMClient;
|
||||
let useFormStatus;
|
||||
let useOptimistic;
|
||||
let useActionState;
|
||||
let Scheduler;
|
||||
|
||||
describe('ReactDOMFizzForm', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
Scheduler = require('scheduler');
|
||||
patchMessageChannel(Scheduler);
|
||||
React = require('react');
|
||||
ReactDOMServer = require('react-dom/server.browser');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
@@ -48,6 +52,14 @@ describe('ReactDOMFizzForm', () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
async function serverAct(callback) {
|
||||
let maybePromise;
|
||||
await act(() => {
|
||||
maybePromise = callback();
|
||||
});
|
||||
return maybePromise;
|
||||
}
|
||||
|
||||
function submit(submitter) {
|
||||
const form = submitter.form || submitter;
|
||||
if (!submitter.form) {
|
||||
@@ -96,7 +108,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
await act(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
@@ -143,7 +157,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
await act(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
@@ -175,7 +191,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
await expect(async () => {
|
||||
await act(async () => {
|
||||
@@ -197,7 +215,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
// This should ideally warn because only the client provides a function that doesn't line up.
|
||||
await act(async () => {
|
||||
@@ -231,7 +251,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
let root;
|
||||
await act(async () => {
|
||||
@@ -278,7 +300,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
let root;
|
||||
await act(async () => {
|
||||
@@ -334,7 +358,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
// Specifying the extra form fields are a DEV error, but we expect it
|
||||
// to eventually still be patched up after an update.
|
||||
await expect(async () => {
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
}).toErrorDev([
|
||||
'Cannot specify a encType or method for a form that specifies a function as the action.',
|
||||
@@ -379,7 +405,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
return 'Pending: ' + pending;
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
expect(container.textContent).toBe('Pending: false');
|
||||
|
||||
@@ -400,7 +428,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
|
||||
// Dispatch an event before hydration
|
||||
@@ -441,7 +471,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
|
||||
submit(container.getElementsByTagName('input')[1]);
|
||||
@@ -463,7 +495,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
return optimisticState;
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
expect(container.textContent).toBe('hi');
|
||||
|
||||
@@ -484,7 +518,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
expect(container.textContent).toBe('0');
|
||||
|
||||
@@ -521,7 +557,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
|
||||
const form = container.firstChild;
|
||||
@@ -581,7 +619,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
|
||||
const input = container.getElementsByTagName('input')[1];
|
||||
@@ -651,7 +691,9 @@ describe('ReactDOMFizzForm', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMServer.renderToReadableStream(<App />),
|
||||
);
|
||||
await readIntoContainer(stream);
|
||||
|
||||
const barField = container.querySelector('[name=bar]');
|
||||
|
||||
+191
-148
@@ -9,6 +9,8 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
@@ -17,15 +19,33 @@ global.TextEncoder = require('util').TextEncoder;
|
||||
let React;
|
||||
let ReactDOMFizzServer;
|
||||
let Suspense;
|
||||
let Scheduler;
|
||||
let act;
|
||||
|
||||
describe('ReactDOMFizzServerBrowser', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
Scheduler = require('scheduler');
|
||||
patchMessageChannel(Scheduler);
|
||||
act = require('internal-test-utils').act;
|
||||
|
||||
React = require('react');
|
||||
ReactDOMFizzServer = require('react-dom/server.browser');
|
||||
Suspense = React.Suspense;
|
||||
});
|
||||
|
||||
async function serverAct(callback) {
|
||||
let maybePromise;
|
||||
await act(() => {
|
||||
maybePromise = callback();
|
||||
if (maybePromise && typeof maybePromise.catch === 'function') {
|
||||
maybePromise.catch(() => {});
|
||||
}
|
||||
});
|
||||
return maybePromise;
|
||||
}
|
||||
|
||||
const theError = new Error('This is an error');
|
||||
function Throw() {
|
||||
throw theError;
|
||||
@@ -48,18 +68,20 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
}
|
||||
|
||||
it('should call renderToReadableStream', async () => {
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>hello world</div>,
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(<div>hello world</div>),
|
||||
);
|
||||
const result = await readResult(stream);
|
||||
expect(result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
|
||||
});
|
||||
|
||||
it('should emit DOCTYPE at the root of the document', async () => {
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<html>
|
||||
<body>hello world</body>
|
||||
</html>,
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<html>
|
||||
<body>hello world</body>
|
||||
</html>,
|
||||
),
|
||||
);
|
||||
const result = await readResult(stream);
|
||||
expect(result).toMatchInlineSnapshot(
|
||||
@@ -68,13 +90,12 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
});
|
||||
|
||||
it('should emit bootstrap script src at the end', async () => {
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>hello world</div>,
|
||||
{
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(<div>hello world</div>, {
|
||||
bootstrapScriptContent: 'INIT();',
|
||||
bootstrapScripts: ['init.js'],
|
||||
bootstrapModules: ['init.mjs'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = await readResult(stream);
|
||||
expect(result).toMatchInlineSnapshot(
|
||||
@@ -93,23 +114,22 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
return 'Done';
|
||||
}
|
||||
let isComplete = false;
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback="Loading">
|
||||
<Wait />
|
||||
</Suspense>
|
||||
</div>,
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback="Loading">
|
||||
<Wait />
|
||||
</Suspense>
|
||||
</div>,
|
||||
),
|
||||
);
|
||||
|
||||
stream.allReady.then(() => (isComplete = true));
|
||||
|
||||
await jest.runAllTimers();
|
||||
expect(isComplete).toBe(false);
|
||||
// Resolve the loading.
|
||||
hasLoaded = true;
|
||||
await resolve();
|
||||
|
||||
await jest.runAllTimers();
|
||||
await serverAct(() => resolve());
|
||||
|
||||
expect(isComplete).toBe(true);
|
||||
|
||||
@@ -123,15 +143,17 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
const reportedErrors = [];
|
||||
let caughtError = null;
|
||||
try {
|
||||
await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Throw />
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Throw />
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
@@ -144,17 +166,19 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
const reportedErrors = [];
|
||||
let caughtError = null;
|
||||
try {
|
||||
await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<Throw />}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<Throw />}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
@@ -165,17 +189,19 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
|
||||
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
|
||||
const reportedErrors = [];
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<Throw />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<Throw />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await readResult(stream);
|
||||
@@ -186,18 +212,20 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
it('should be able to complete by aborting even if the promise never resolves', async () => {
|
||||
const errors = [];
|
||||
const controller = new AbortController();
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
controller.abort();
|
||||
@@ -211,20 +239,20 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
it('should reject if aborting before the shell is complete', async () => {
|
||||
const errors = [];
|
||||
const controller = new AbortController();
|
||||
const promise = ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<InfiniteSuspend />
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
const promise = serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<InfiniteSuspend />
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await jest.runAllTimers();
|
||||
|
||||
const theReason = new Error('aborted for reasons');
|
||||
controller.abort(theReason);
|
||||
|
||||
@@ -249,16 +277,18 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
const streamPromise = ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<App />
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
const streamPromise = serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<App />
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
let caughtError = null;
|
||||
@@ -277,18 +307,20 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
const theReason = new Error('aborted for reasons');
|
||||
controller.abort(theReason);
|
||||
|
||||
const promise = ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
const promise = serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Technically we could still continue rendering the shell but currently the
|
||||
@@ -317,17 +349,19 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
return 'Done';
|
||||
}
|
||||
const errors = [];
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<Wait />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<Wait />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
stream.allReady.then(() => (isComplete = true));
|
||||
@@ -344,9 +378,7 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
]);
|
||||
|
||||
hasLoaded = true;
|
||||
resolve();
|
||||
|
||||
await jest.runAllTimers();
|
||||
await serverAct(() => resolve());
|
||||
|
||||
expect(rendered).toBe(false);
|
||||
expect(isComplete).toBe(true);
|
||||
@@ -366,14 +398,16 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
// as such for now. I don't think it needs to be maintained if in the future
|
||||
// the view sizes change or become dynamic becasue of the use of byobRequest
|
||||
let stream;
|
||||
stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<>
|
||||
<div>
|
||||
<span>{''}</span>
|
||||
</div>
|
||||
<div>{str492}</div>
|
||||
<div>{str492}</div>
|
||||
</>,
|
||||
stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<>
|
||||
<div>
|
||||
<span>{''}</span>
|
||||
</div>
|
||||
<div>{str492}</div>
|
||||
<div>{str492}</div>
|
||||
</>,
|
||||
),
|
||||
);
|
||||
|
||||
let result;
|
||||
@@ -385,10 +419,12 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
// this size 2049 was chosen to be a couple base 2 orders larger than the current view
|
||||
// size. if the size changes in the future hopefully this will still exercise
|
||||
// a chunk that is too large for the view size.
|
||||
stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<>
|
||||
<div>{str2049}</div>
|
||||
</>,
|
||||
stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<>
|
||||
<div>{str2049}</div>
|
||||
</>,
|
||||
),
|
||||
);
|
||||
|
||||
result = await readResult(stream);
|
||||
@@ -419,13 +455,15 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
|
||||
const errors = [];
|
||||
const controller = new AbortController();
|
||||
await ReactDOMFizzServer.renderToReadableStream(<App />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x);
|
||||
return 'a digest';
|
||||
},
|
||||
});
|
||||
await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(<App />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x);
|
||||
return 'a digest';
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
controller.abort('foobar');
|
||||
|
||||
@@ -456,13 +494,15 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
|
||||
const errors = [];
|
||||
const controller = new AbortController();
|
||||
await ReactDOMFizzServer.renderToReadableStream(<App />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
return 'a digest';
|
||||
},
|
||||
});
|
||||
await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(<App />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
return 'a digest';
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
controller.abort(new Error('uh oh'));
|
||||
|
||||
@@ -471,13 +511,15 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
|
||||
// https://github.com/facebook/react/pull/25534/files - fix transposed escape functions
|
||||
it('should encode title properly', async () => {
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<html>
|
||||
<head>
|
||||
<title>foo</title>
|
||||
</head>
|
||||
<body>bar</body>
|
||||
</html>,
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(
|
||||
<html>
|
||||
<head>
|
||||
<title>foo</title>
|
||||
</head>
|
||||
<body>bar</body>
|
||||
</html>,
|
||||
),
|
||||
);
|
||||
|
||||
const result = await readResult(stream);
|
||||
@@ -488,14 +530,13 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
|
||||
it('should support nonce attribute for bootstrap scripts', async () => {
|
||||
const nonce = 'R4nd0m';
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>hello world</div>,
|
||||
{
|
||||
const stream = await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(<div>hello world</div>, {
|
||||
nonce,
|
||||
bootstrapScriptContent: 'INIT();',
|
||||
bootstrapScripts: ['init.js'],
|
||||
bootstrapModules: ['init.mjs'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = await readResult(stream);
|
||||
expect(result).toMatchInlineSnapshot(
|
||||
@@ -523,14 +564,16 @@ describe('ReactDOMFizzServerBrowser', () => {
|
||||
|
||||
let caughtError = null;
|
||||
try {
|
||||
await ReactDOMFizzServer.renderToReadableStream(<App />, {
|
||||
onError(error) {
|
||||
errors.push(error.message);
|
||||
},
|
||||
onPostpone(reason) {
|
||||
postponed.push(reason);
|
||||
},
|
||||
});
|
||||
await serverAct(() =>
|
||||
ReactDOMFizzServer.renderToReadableStream(<App />, {
|
||||
onError(error) {
|
||||
errors.push(error.message);
|
||||
},
|
||||
onPostpone(reason) {
|
||||
postponed.push(reason);
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
}
|
||||
|
||||
+315
-198
@@ -9,6 +9,8 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
|
||||
|
||||
import {
|
||||
getVisibleChildren,
|
||||
insertNodesAndExecuteScripts,
|
||||
@@ -26,10 +28,17 @@ let ReactDOMFizzServer;
|
||||
let ReactDOMFizzStatic;
|
||||
let Suspense;
|
||||
let container;
|
||||
let Scheduler;
|
||||
let act;
|
||||
|
||||
describe('ReactDOMFizzStaticBrowser', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
Scheduler = require('scheduler');
|
||||
patchMessageChannel(Scheduler);
|
||||
act = require('internal-test-utils').act;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMFizzServer = require('react-dom/server.browser');
|
||||
@@ -45,6 +54,17 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
async function serverAct(callback) {
|
||||
let maybePromise;
|
||||
await act(() => {
|
||||
maybePromise = callback();
|
||||
if (maybePromise && typeof maybePromise.catch === 'function') {
|
||||
maybePromise.catch(() => {});
|
||||
}
|
||||
});
|
||||
return maybePromise;
|
||||
}
|
||||
|
||||
const theError = new Error('This is an error');
|
||||
function Throw() {
|
||||
throw theError;
|
||||
@@ -113,17 +133,21 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
// @gate experimental
|
||||
it('should call prerender', async () => {
|
||||
const result = await ReactDOMFizzStatic.prerender(<div>hello world</div>);
|
||||
const result = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<div>hello world</div>),
|
||||
);
|
||||
const prelude = await readContent(result.prelude);
|
||||
expect(prelude).toMatchInlineSnapshot(`"<div>hello world</div>"`);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should emit DOCTYPE at the root of the document', async () => {
|
||||
const result = await ReactDOMFizzStatic.prerender(
|
||||
<html>
|
||||
<body>hello world</body>
|
||||
</html>,
|
||||
const result = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(
|
||||
<html>
|
||||
<body>hello world</body>
|
||||
</html>,
|
||||
),
|
||||
);
|
||||
const prelude = await readContent(result.prelude);
|
||||
expect(prelude).toMatchInlineSnapshot(
|
||||
@@ -133,11 +157,13 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
// @gate experimental
|
||||
it('should emit bootstrap script src at the end', async () => {
|
||||
const result = await ReactDOMFizzStatic.prerender(<div>hello world</div>, {
|
||||
bootstrapScriptContent: 'INIT();',
|
||||
bootstrapScripts: ['init.js'],
|
||||
bootstrapModules: ['init.mjs'],
|
||||
});
|
||||
const result = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<div>hello world</div>, {
|
||||
bootstrapScriptContent: 'INIT();',
|
||||
bootstrapScripts: ['init.js'],
|
||||
bootstrapModules: ['init.mjs'],
|
||||
}),
|
||||
);
|
||||
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>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
|
||||
@@ -155,12 +181,14 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
}
|
||||
return 'Done';
|
||||
}
|
||||
const resultPromise = ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback="Loading">
|
||||
<Wait />
|
||||
</Suspense>
|
||||
</div>,
|
||||
const resultPromise = serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback="Loading">
|
||||
<Wait />
|
||||
</Suspense>
|
||||
</div>,
|
||||
),
|
||||
);
|
||||
|
||||
await jest.runAllTimers();
|
||||
@@ -171,9 +199,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
const result = await resultPromise;
|
||||
const prelude = await readContent(result.prelude);
|
||||
expect(prelude).toMatchInlineSnapshot(
|
||||
`"<div><!--$-->Done<!-- --><!--/$--></div>"`,
|
||||
);
|
||||
expect(prelude).toMatchInlineSnapshot(`"<div><!--$-->Done<!--/$--></div>"`);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
@@ -181,15 +207,17 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
const reportedErrors = [];
|
||||
let caughtError = null;
|
||||
try {
|
||||
await ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Throw />
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Throw />
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
@@ -203,17 +231,19 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
const reportedErrors = [];
|
||||
let caughtError = null;
|
||||
try {
|
||||
await ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback={<Throw />}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback={<Throw />}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
@@ -225,17 +255,19 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
// @gate experimental
|
||||
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
|
||||
const reportedErrors = [];
|
||||
const result = await ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<Throw />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
const result = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<Throw />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
onError(x) {
|
||||
reportedErrors.push(x);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const prelude = await readContent(result.prelude);
|
||||
@@ -247,21 +279,22 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
it('should be able to complete by aborting even if the promise never resolves', async () => {
|
||||
const errors = [];
|
||||
const controller = new AbortController();
|
||||
const resultPromise = ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
let resultPromise;
|
||||
await serverAct(() => {
|
||||
resultPromise = ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await jest.runAllTimers();
|
||||
);
|
||||
});
|
||||
|
||||
controller.abort();
|
||||
|
||||
@@ -277,16 +310,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
it('should reject if aborting before the shell is complete', async () => {
|
||||
const errors = [];
|
||||
const controller = new AbortController();
|
||||
const promise = ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<InfiniteSuspend />
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
const promise = serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<InfiniteSuspend />
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await jest.runAllTimers();
|
||||
@@ -316,16 +351,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
const streamPromise = ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<App />
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
const streamPromise = serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<App />
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
let caughtError = null;
|
||||
@@ -345,18 +382,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
const theReason = new Error('aborted for reasons');
|
||||
controller.abort(theReason);
|
||||
|
||||
const promise = ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
const promise = serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(
|
||||
<div>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<InfiniteSuspend />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Technically we could still continue rendering the shell but currently the
|
||||
@@ -396,12 +435,15 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
const errors = [];
|
||||
const controller = new AbortController();
|
||||
const resultPromise = ReactDOMFizzStatic.prerender(<App />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x);
|
||||
return 'a digest';
|
||||
},
|
||||
let resultPromise;
|
||||
await serverAct(() => {
|
||||
resultPromise = ReactDOMFizzStatic.prerender(<App />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x);
|
||||
return 'a digest';
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
controller.abort('foobar');
|
||||
@@ -436,12 +478,15 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
const errors = [];
|
||||
const controller = new AbortController();
|
||||
const resultPromise = ReactDOMFizzStatic.prerender(<App />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
return 'a digest';
|
||||
},
|
||||
let resultPromise;
|
||||
await serverAct(() => {
|
||||
resultPromise = ReactDOMFizzStatic.prerender(<App />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
return 'a digest';
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
controller.abort(new Error('uh oh'));
|
||||
@@ -471,14 +516,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const resumed = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const resumed = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -513,14 +562,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const resumed = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const resumed = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -552,14 +605,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const resumed = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const resumed = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -600,14 +657,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const resumed = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const resumed = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -641,14 +702,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const resumed = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const resumed = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -682,14 +747,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const content = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const content = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
const html = await readContent(concat(prerendered.prelude, content));
|
||||
@@ -748,9 +817,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
{virtual: true},
|
||||
);
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
|
||||
bootstrapScripts: ['init.js'],
|
||||
});
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />, {
|
||||
bootstrapScripts: ['init.js'],
|
||||
}),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -779,9 +850,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
]);
|
||||
|
||||
prerendering = false;
|
||||
const content = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const content = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(content);
|
||||
@@ -860,14 +933,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const resumed = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const resumed = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -911,14 +988,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const resumed = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const resumed = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -957,7 +1038,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
// TODO: This should actually be null because we should've been able to fully
|
||||
// resolve the render on the server eventually, even though the fallback postponed.
|
||||
// So we should not need to resume.
|
||||
@@ -967,9 +1050,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
|
||||
|
||||
const resumed = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const resumed = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(resumed);
|
||||
@@ -1020,7 +1105,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -1033,14 +1120,16 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
prerendering = false;
|
||||
|
||||
const errors = [];
|
||||
const resumed = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
{
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
const resumed = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
{
|
||||
onError(x) {
|
||||
errors.push(x.message);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(errors).toEqual([
|
||||
@@ -1085,7 +1174,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -1098,15 +1189,17 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
const errors = [];
|
||||
|
||||
const resumedPromise = ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x);
|
||||
const resumedPromise = serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
{
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x);
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
controller.abort('abort');
|
||||
@@ -1160,16 +1253,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const resumedPromise = ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const resumedPromise = serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await jest.runAllTimers();
|
||||
@@ -1204,16 +1301,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
expect(await readContent(prerendered.prelude)).toBe('');
|
||||
|
||||
const content = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const content = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
expect(await readContent(content)).toBe(
|
||||
@@ -1246,16 +1347,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
expect(await readContent(prerendered.prelude)).toBe('');
|
||||
|
||||
const content = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const content = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
expect(await readContent(content)).toBe(
|
||||
@@ -1293,16 +1398,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
|
||||
expect(await readContent(prerendered.prelude)).toBe('');
|
||||
|
||||
const content = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const content = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
expect(await readContent(content)).toBe(
|
||||
@@ -1356,9 +1465,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
|
||||
onHeaders,
|
||||
});
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />, {
|
||||
onHeaders,
|
||||
}),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
prerendering = false;
|
||||
@@ -1375,9 +1486,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const content = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const content = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
@@ -1391,7 +1504,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
await 1;
|
||||
hasLoaded = true;
|
||||
resolve();
|
||||
await serverAct(resolve);
|
||||
|
||||
while (true) {
|
||||
({value, done} = await reader.read());
|
||||
@@ -1425,10 +1538,12 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
throw new Error('bad onHeaders');
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<div>hello</div>, {
|
||||
onHeaders,
|
||||
onError,
|
||||
});
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<div>hello</div>, {
|
||||
onHeaders,
|
||||
onError,
|
||||
}),
|
||||
);
|
||||
expect(prerendered.postponed).toBe(null);
|
||||
expect(errors).toEqual(['bad onHeaders']);
|
||||
|
||||
@@ -1469,9 +1584,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
{virtual: true},
|
||||
);
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
|
||||
bootstrapScripts: ['init.js'],
|
||||
});
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />, {
|
||||
bootstrapScripts: ['init.js'],
|
||||
}),
|
||||
);
|
||||
|
||||
const postponedSerializedState = JSON.stringify(prerendered.postponed);
|
||||
|
||||
@@ -1497,9 +1614,8 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
prerendering = false;
|
||||
|
||||
const content = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(postponedSerializedState),
|
||||
const content = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedSerializedState)),
|
||||
);
|
||||
|
||||
await readIntoContainer(content);
|
||||
@@ -1542,7 +1658,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
const postponedState = JSON.stringify(prerendered.postponed);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -1550,9 +1668,8 @@ describe('ReactDOMFizzStaticBrowser', () => {
|
||||
|
||||
isPrerendering = false;
|
||||
|
||||
const dynamic = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(postponedState),
|
||||
const dynamic = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
|
||||
);
|
||||
|
||||
await readIntoContainer(dynamic);
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
|
||||
|
||||
import {
|
||||
getVisibleChildren,
|
||||
insertNodesAndExecuteScripts,
|
||||
@@ -25,10 +27,16 @@ let ReactDOMFizzServer;
|
||||
let ReactDOMFizzStatic;
|
||||
let Suspense;
|
||||
let container;
|
||||
let Scheduler;
|
||||
let act;
|
||||
|
||||
describe('ReactDOMFizzStaticFloat', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
Scheduler = require('scheduler');
|
||||
patchMessageChannel(Scheduler);
|
||||
act = require('internal-test-utils').act;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMFizzServer = require('react-dom/server.browser');
|
||||
@@ -44,6 +52,17 @@ describe('ReactDOMFizzStaticFloat', () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
async function serverAct(callback) {
|
||||
let maybePromise;
|
||||
await act(() => {
|
||||
maybePromise = callback();
|
||||
if (maybePromise && typeof maybePromise.catch === 'function') {
|
||||
maybePromise.catch(() => {});
|
||||
}
|
||||
});
|
||||
return maybePromise;
|
||||
}
|
||||
|
||||
async function readIntoContainer(stream) {
|
||||
const reader = stream.getReader();
|
||||
let result = '';
|
||||
@@ -135,7 +154,9 @@ describe('ReactDOMFizzStaticFloat', () => {
|
||||
virtual: true,
|
||||
});
|
||||
|
||||
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
|
||||
const prerendered = await serverAct(() =>
|
||||
ReactDOMFizzStatic.prerender(<App />),
|
||||
);
|
||||
expect(prerendered.postponed).not.toBe(null);
|
||||
|
||||
await readIntoContainer(prerendered.prelude);
|
||||
@@ -171,28 +192,28 @@ describe('ReactDOMFizzStaticFloat', () => {
|
||||
]);
|
||||
|
||||
prerendering = false;
|
||||
const content = await ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
const content = await serverAct(() =>
|
||||
ReactDOMFizzServer.resume(
|
||||
<App />,
|
||||
JSON.parse(JSON.stringify(prerendered.postponed)),
|
||||
),
|
||||
);
|
||||
|
||||
await readIntoContainer(content);
|
||||
|
||||
// Dispatch load event to injected stylesheet
|
||||
const linkCreds = document.querySelector(
|
||||
'link[rel="stylesheet"][href="style creds"]',
|
||||
);
|
||||
const linkAnon = document.querySelector(
|
||||
'link[rel="stylesheet"][href="style anon"]',
|
||||
);
|
||||
const event = document.createEvent('Events');
|
||||
event.initEvent('load', true, true);
|
||||
linkCreds.dispatchEvent(event);
|
||||
linkAnon.dispatchEvent(event);
|
||||
|
||||
// Wait for the instruction microtasks to flush.
|
||||
await 0;
|
||||
await 0;
|
||||
await act(() => {
|
||||
// Dispatch load event to injected stylesheet
|
||||
const linkCreds = document.querySelector(
|
||||
'link[rel="stylesheet"][href="style creds"]',
|
||||
);
|
||||
const linkAnon = document.querySelector(
|
||||
'link[rel="stylesheet"][href="style anon"]',
|
||||
);
|
||||
const event = document.createEvent('Events');
|
||||
event.initEvent('load', true, true);
|
||||
linkCreds.dispatchEvent(event);
|
||||
linkAnon.dispatchEvent(event);
|
||||
});
|
||||
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html>
|
||||
|
||||
+150
-50
@@ -1020,15 +1020,15 @@ describe('ReactDOMForm', () => {
|
||||
assertLog(['0']);
|
||||
expect(container.textContent).toBe('0');
|
||||
|
||||
await act(() => dispatch('increment'));
|
||||
await act(() => startTransition(() => dispatch('increment')));
|
||||
assertLog(['Async action started [1]', 'Pending 0']);
|
||||
expect(container.textContent).toBe('Pending 0');
|
||||
|
||||
// Dispatch a few more actions. None of these will start until the previous
|
||||
// one finishes.
|
||||
await act(() => dispatch('increment'));
|
||||
await act(() => dispatch('decrement'));
|
||||
await act(() => dispatch('increment'));
|
||||
await act(() => startTransition(() => dispatch('increment')));
|
||||
await act(() => startTransition(() => dispatch('decrement')));
|
||||
await act(() => startTransition(() => dispatch('increment')));
|
||||
assertLog([]);
|
||||
|
||||
// Each action starts as soon as the previous one finishes.
|
||||
@@ -1067,7 +1067,7 @@ describe('ReactDOMForm', () => {
|
||||
|
||||
// Perform an action. This will increase the state by 1, as defined by the
|
||||
// stepSize prop.
|
||||
await act(() => increment());
|
||||
await act(() => startTransition(() => increment()));
|
||||
assertLog(['Pending 0', '1']);
|
||||
|
||||
// Now increase the stepSize prop to 10. Subsequent steps will increase
|
||||
@@ -1076,7 +1076,7 @@ describe('ReactDOMForm', () => {
|
||||
assertLog(['1']);
|
||||
|
||||
// Increment again. The state should increase by 10.
|
||||
await act(() => increment());
|
||||
await act(() => startTransition(() => increment()));
|
||||
assertLog(['Pending 1', '11']);
|
||||
});
|
||||
|
||||
@@ -1113,11 +1113,11 @@ describe('ReactDOMForm', () => {
|
||||
await act(() => root.render(<App />));
|
||||
assertLog(['A']);
|
||||
|
||||
await act(() => action('B'));
|
||||
await act(() => startTransition(() => action('B')));
|
||||
// The first dispatch will update the pending state.
|
||||
assertLog(['Pending A']);
|
||||
await act(() => action('C'));
|
||||
await act(() => action('D'));
|
||||
await act(() => startTransition(() => action('C')));
|
||||
await act(() => startTransition(() => action('D')));
|
||||
assertLog([]);
|
||||
|
||||
await act(() => resolveText('B'));
|
||||
@@ -1151,10 +1151,10 @@ describe('ReactDOMForm', () => {
|
||||
|
||||
// Dispatch two actions. The first one is async, so it forces the second
|
||||
// one into an async queue.
|
||||
await act(() => action('First action'));
|
||||
await act(() => startTransition(() => action('First action')));
|
||||
assertLog(['Initial (pending)']);
|
||||
// This action won't run until the first one finishes.
|
||||
await act(() => action('Second action'));
|
||||
await act(() => startTransition(() => action('Second action')));
|
||||
|
||||
// While the first action is still pending, update a prop. This causes the
|
||||
// inline action implementation to change, but it should not affect the
|
||||
@@ -1169,7 +1169,9 @@ describe('ReactDOMForm', () => {
|
||||
|
||||
// Confirm that if we dispatch yet another action, it uses the updated
|
||||
// action implementation.
|
||||
await expect(act(() => action('Third action'))).rejects.toThrow('Oops!');
|
||||
await expect(
|
||||
act(() => startTransition(() => action('Third action'))),
|
||||
).rejects.toThrow('Oops!');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1192,7 +1194,7 @@ describe('ReactDOMForm', () => {
|
||||
|
||||
// Perform an action. This will increase the state by 1, as defined by the
|
||||
// stepSize prop.
|
||||
await act(() => increment());
|
||||
await act(() => startTransition(() => increment()));
|
||||
assertLog(['Pending 0', '1']);
|
||||
|
||||
// Now increase the stepSize prop to 10. Subsequent steps will increase
|
||||
@@ -1201,7 +1203,7 @@ describe('ReactDOMForm', () => {
|
||||
assertLog(['1']);
|
||||
|
||||
// Increment again. The state should increase by 10.
|
||||
await act(() => increment());
|
||||
await act(() => startTransition(() => increment()));
|
||||
assertLog(['Pending 1', '11']);
|
||||
});
|
||||
|
||||
@@ -1219,12 +1221,12 @@ describe('ReactDOMForm', () => {
|
||||
await act(() => root.render(<App />));
|
||||
assertLog(['A']);
|
||||
|
||||
await act(() => action(getText('B')));
|
||||
await act(() => startTransition(() => action(getText('B'))));
|
||||
// The first dispatch will update the pending state.
|
||||
assertLog(['Pending A']);
|
||||
await act(() => action('C'));
|
||||
await act(() => action(getText('D')));
|
||||
await act(() => action('E'));
|
||||
await act(() => startTransition(() => action('C')));
|
||||
await act(() => startTransition(() => action(getText('D'))));
|
||||
await act(() => startTransition(() => action('E')));
|
||||
assertLog([]);
|
||||
|
||||
await act(() => resolveText('B'));
|
||||
@@ -1235,14 +1237,12 @@ describe('ReactDOMForm', () => {
|
||||
|
||||
// @gate enableAsyncActions
|
||||
test('useActionState: error handling (sync action)', async () => {
|
||||
let resetErrorBoundary;
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
static getDerivedStateFromError(error) {
|
||||
return {error};
|
||||
}
|
||||
render() {
|
||||
resetErrorBoundary = () => this.setState({error: null});
|
||||
if (this.state.error !== null) {
|
||||
return <Text text={'Caught an error: ' + this.state.error.message} />;
|
||||
}
|
||||
@@ -1273,7 +1273,7 @@ describe('ReactDOMForm', () => {
|
||||
);
|
||||
assertLog(['A']);
|
||||
|
||||
await act(() => action('Oops!'));
|
||||
await act(() => startTransition(() => action('Oops!')));
|
||||
assertLog([
|
||||
// Action begins, error has not thrown yet.
|
||||
'Pending A',
|
||||
@@ -1282,31 +1282,16 @@ describe('ReactDOMForm', () => {
|
||||
'Caught an error: Oops!',
|
||||
]);
|
||||
expect(container.textContent).toBe('Caught an error: Oops!');
|
||||
|
||||
// Reset the error boundary
|
||||
await act(() => resetErrorBoundary());
|
||||
assertLog(['A']);
|
||||
|
||||
// Trigger an error again, but this time, perform another action that
|
||||
// overrides the first one and fixes the error
|
||||
await act(() => {
|
||||
action('Oops!');
|
||||
action('B');
|
||||
});
|
||||
assertLog(['Pending A', 'B']);
|
||||
expect(container.textContent).toBe('B');
|
||||
});
|
||||
|
||||
// @gate enableAsyncActions
|
||||
test('useActionState: error handling (async action)', async () => {
|
||||
let resetErrorBoundary;
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
static getDerivedStateFromError(error) {
|
||||
return {error};
|
||||
}
|
||||
render() {
|
||||
resetErrorBoundary = () => this.setState({error: null});
|
||||
if (this.state.error !== null) {
|
||||
return <Text text={'Caught an error: ' + this.state.error.message} />;
|
||||
}
|
||||
@@ -1338,27 +1323,71 @@ describe('ReactDOMForm', () => {
|
||||
);
|
||||
assertLog(['A']);
|
||||
|
||||
await act(() => action('Oops!'));
|
||||
await act(() => startTransition(() => action('Oops!')));
|
||||
// The first dispatch will update the pending state.
|
||||
assertLog(['Pending A']);
|
||||
await act(() => resolveText('Oops!'));
|
||||
assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
|
||||
expect(container.textContent).toBe('Caught an error: Oops!');
|
||||
});
|
||||
|
||||
// Reset the error boundary
|
||||
await act(() => resetErrorBoundary());
|
||||
test('useActionState: when an action errors, subsequent actions are canceled', async () => {
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
static getDerivedStateFromError(error) {
|
||||
return {error};
|
||||
}
|
||||
render() {
|
||||
if (this.state.error !== null) {
|
||||
return <Text text={'Caught an error: ' + this.state.error.message} />;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
let action;
|
||||
function App() {
|
||||
const [state, dispatch, isPending] = useActionState(async (s, a) => {
|
||||
Scheduler.log('Start action: ' + a);
|
||||
const text = await getText(a);
|
||||
if (text.endsWith('!')) {
|
||||
throw new Error(text);
|
||||
}
|
||||
return text;
|
||||
}, 'A');
|
||||
action = dispatch;
|
||||
const pending = isPending ? 'Pending ' : '';
|
||||
return <Text text={pending + state} />;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() =>
|
||||
root.render(
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>,
|
||||
),
|
||||
);
|
||||
assertLog(['A']);
|
||||
|
||||
// Trigger an error again, but this time, perform another action that
|
||||
// overrides the first one and fixes the error
|
||||
await act(() => {
|
||||
action('Oops!');
|
||||
action('B');
|
||||
});
|
||||
assertLog(['Pending A']);
|
||||
await act(() => resolveText('B'));
|
||||
assertLog(['B']);
|
||||
expect(container.textContent).toBe('B');
|
||||
await act(() => startTransition(() => action('Oops!')));
|
||||
assertLog(['Start action: Oops!', 'Pending A']);
|
||||
|
||||
// Queue up another action after the one will error.
|
||||
await act(() => startTransition(() => action('Should never run')));
|
||||
assertLog([]);
|
||||
|
||||
// The first dispatch will update the pending state.
|
||||
await act(() => resolveText('Oops!'));
|
||||
assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
|
||||
expect(container.textContent).toBe('Caught an error: Oops!');
|
||||
|
||||
// Attempt to dispatch another action. This should not run either.
|
||||
await act(() =>
|
||||
startTransition(() => action('This also should never run')),
|
||||
);
|
||||
assertLog([]);
|
||||
expect(container.textContent).toBe('Caught an error: Oops!');
|
||||
});
|
||||
|
||||
// @gate enableAsyncActions
|
||||
@@ -1399,7 +1428,7 @@ describe('ReactDOMForm', () => {
|
||||
assertLog(['0']);
|
||||
expect(container.textContent).toBe('0');
|
||||
|
||||
await act(() => dispatch('increment'));
|
||||
await act(() => startTransition(() => dispatch('increment')));
|
||||
assertLog(['Async action started [1]', 'Pending 0']);
|
||||
expect(container.textContent).toBe('Pending 0');
|
||||
|
||||
@@ -1408,6 +1437,77 @@ describe('ReactDOMForm', () => {
|
||||
expect(container.textContent).toBe('1');
|
||||
});
|
||||
|
||||
test('useActionState does not wrap action in a transition unless dispatch is in a transition', async () => {
|
||||
let dispatch;
|
||||
function App() {
|
||||
const [state, _dispatch] = useActionState(() => {
|
||||
return state + 1;
|
||||
}, 0);
|
||||
dispatch = _dispatch;
|
||||
return <AsyncText text={'Count: ' + state} />;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() =>
|
||||
root.render(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>,
|
||||
),
|
||||
);
|
||||
assertLog(['Suspend! [Count: 0]', 'Loading...']);
|
||||
await act(() => resolveText('Count: 0'));
|
||||
assertLog(['Count: 0']);
|
||||
|
||||
// Dispatch outside of a transition. This will trigger a loading state.
|
||||
await act(() => dispatch());
|
||||
assertLog(['Suspend! [Count: 1]', 'Loading...']);
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
await act(() => resolveText('Count: 1'));
|
||||
assertLog(['Count: 1']);
|
||||
expect(container.textContent).toBe('Count: 1');
|
||||
|
||||
// Now dispatch inside of a transition. This one does not trigger a
|
||||
// loading state.
|
||||
await act(() => startTransition(() => dispatch()));
|
||||
assertLog(['Count: 1', 'Suspend! [Count: 2]', 'Loading...']);
|
||||
expect(container.textContent).toBe('Count: 1');
|
||||
|
||||
await act(() => resolveText('Count: 2'));
|
||||
assertLog(['Count: 2']);
|
||||
expect(container.textContent).toBe('Count: 2');
|
||||
});
|
||||
|
||||
test('useActionState warns if async action is dispatched outside of a transition', async () => {
|
||||
let dispatch;
|
||||
function App() {
|
||||
const [state, _dispatch] = useActionState(async () => {
|
||||
return state + 1;
|
||||
}, 0);
|
||||
dispatch = _dispatch;
|
||||
return <AsyncText text={'Count: ' + state} />;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => root.render(<App />));
|
||||
assertLog(['Suspend! [Count: 0]']);
|
||||
await act(() => resolveText('Count: 0'));
|
||||
assertLog(['Count: 0']);
|
||||
|
||||
// Dispatch outside of a transition.
|
||||
await act(() => dispatch());
|
||||
assertConsoleErrorDev([
|
||||
[
|
||||
'An async function was passed to useActionState, but it was ' +
|
||||
'dispatched outside of an action context',
|
||||
{withoutStack: true},
|
||||
],
|
||||
]);
|
||||
assertLog(['Suspend! [Count: 1]']);
|
||||
expect(container.textContent).toBe('Count: 0');
|
||||
});
|
||||
|
||||
test('uncontrolled form inputs are reset after the action completes', async () => {
|
||||
const formRef = React.createRef();
|
||||
const inputRef = React.createRef();
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let JSDOM;
|
||||
let React;
|
||||
let ReactDOMClient;
|
||||
let container;
|
||||
let waitForAll;
|
||||
|
||||
describe('ReactDOM HostSingleton', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
JSDOM = require('jsdom').JSDOM;
|
||||
// Test Environment
|
||||
const jsdom = new JSDOM(
|
||||
'<!DOCTYPE html><html><head></head><body><div id="container">',
|
||||
{
|
||||
runScripts: 'dangerously',
|
||||
},
|
||||
);
|
||||
global.window = jsdom.window;
|
||||
global.document = jsdom.window.document;
|
||||
container = global.document.getElementById('container');
|
||||
|
||||
React = require('react');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
waitForAll = InternalTestUtils.waitForAll;
|
||||
});
|
||||
|
||||
it('errors when a hoistable component becomes a Resource', async () => {
|
||||
const errors = [];
|
||||
function onError(e) {
|
||||
errors.push(e.message);
|
||||
}
|
||||
const root = ReactDOMClient.createRoot(container, {
|
||||
onUncaughtError: onError,
|
||||
});
|
||||
|
||||
root.render(
|
||||
<div>
|
||||
<link rel="preload" href="bar" as="style" />
|
||||
</div>,
|
||||
);
|
||||
await waitForAll([]);
|
||||
|
||||
root.render(
|
||||
<div>
|
||||
<link rel="stylesheet" href="bar" precedence="default" />
|
||||
</div>,
|
||||
);
|
||||
await waitForAll([]);
|
||||
if (__DEV__) {
|
||||
expect(errors).toEqual([
|
||||
`Expected <link> not to update to be updated to a stylesheet with precedence. Check the \`rel\`, \`href\`, and \`precedence\` props of this component. Alternatively, check whether two different <link> components render in the same slot or share the same key.
|
||||
|
||||
- <link rel=\"preload\" href=\"bar\" ... />
|
||||
+ <link rel=\"stylesheet\" href=\"bar\" precedence=\"default\" />`,
|
||||
]);
|
||||
} else {
|
||||
expect(errors).toEqual([
|
||||
'Expected <link> not to update to be updated to a stylesheet with precedence. Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different <link> components render in the same slot or share the same key.',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('errors when a hoistable Resource becomes an instance', async () => {
|
||||
const errors = [];
|
||||
function onError(e) {
|
||||
errors.push(e.message);
|
||||
}
|
||||
const root = ReactDOMClient.createRoot(container, {
|
||||
onUncaughtError: onError,
|
||||
});
|
||||
|
||||
root.render(
|
||||
<div>
|
||||
<link rel="stylesheet" href="bar" precedence="default" />
|
||||
</div>,
|
||||
);
|
||||
await waitForAll([]);
|
||||
const event = new window.Event('load');
|
||||
const preloads = document.querySelectorAll('link[rel="preload"]');
|
||||
for (let i = 0; i < preloads.length; i++) {
|
||||
const node = preloads[i];
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
const stylesheets = document.querySelectorAll('link[rel="preload"]');
|
||||
for (let i = 0; i < stylesheets.length; i++) {
|
||||
const node = stylesheets[i];
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
root.render(
|
||||
<div>
|
||||
<link rel="foo" href="bar" />
|
||||
</div>,
|
||||
);
|
||||
await waitForAll([]);
|
||||
if (__DEV__) {
|
||||
expect(errors).toEqual([
|
||||
`Expected stylesheet with precedence to not be updated to a different kind of <link>. Check the \`rel\`, \`href\`, and \`precedence\` props of this component. Alternatively, check whether two different <link> components render in the same slot or share the same key.
|
||||
|
||||
- <link rel=\"stylesheet\" href=\"bar\" precedence=\"default\" />
|
||||
+ <link rel=\"foo\" href=\"bar\" />`,
|
||||
]);
|
||||
} else {
|
||||
expect(errors).toEqual([
|
||||
'Expected stylesheet with precedence to not be updated to a different kind of <link>. Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different <link> components render in the same slot or share the same key.',
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,10 @@ import {
|
||||
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
import {enableAddPropertiesFastPath} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
enableAddPropertiesFastPath,
|
||||
enableShallowPropDiffing,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import type {AttributeConfiguration} from './ReactNativeTypes';
|
||||
|
||||
@@ -342,7 +345,7 @@ function diffProperties(
|
||||
// Pattern match on: attributeConfig
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
if (defaultDiffer(prevProp, nextProp)) {
|
||||
if (enableShallowPropDiffing || defaultDiffer(prevProp, nextProp)) {
|
||||
// a normal leaf has changed
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
@@ -354,6 +357,7 @@ function diffProperties(
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const shouldUpdate =
|
||||
enableShallowPropDiffing ||
|
||||
prevProp === undefined ||
|
||||
(typeof attributeConfig.diff === 'function'
|
||||
? attributeConfig.diff(prevProp, nextProp)
|
||||
@@ -449,17 +453,24 @@ function fastAddProperties(
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
let attributeConfig;
|
||||
let prop;
|
||||
// Flatten nested style props.
|
||||
if (isArray(props)) {
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
payload = fastAddProperties(payload, props[i], validAttributes);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
for (const propKey in props) {
|
||||
prop = props[propKey];
|
||||
const prop = props[propKey];
|
||||
|
||||
if (prop === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
attributeConfig = ((validAttributes[propKey]: any): AttributeConfiguration);
|
||||
const attributeConfig = ((validAttributes[
|
||||
propKey
|
||||
]: any): AttributeConfiguration);
|
||||
|
||||
if (attributeConfig == null) {
|
||||
continue;
|
||||
@@ -477,7 +488,7 @@ function fastAddProperties(
|
||||
// An atomic prop with custom processing.
|
||||
newValue = attributeConfig.process(prop);
|
||||
} else if (typeof attributeConfig.diff === 'function') {
|
||||
// An atomic prop with custom diffing. We don't do diffing here.
|
||||
// An atomic prop with custom diffing. We don't need to do diffing when adding props.
|
||||
newValue = prop;
|
||||
}
|
||||
|
||||
@@ -489,17 +500,6 @@ function fastAddProperties(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not-atomic prop that needs to be flattened. Likely it's the 'style' prop.
|
||||
|
||||
// It can be an array.
|
||||
if (isArray(prop)) {
|
||||
for (let i = 0; i < prop.length; i++) {
|
||||
payload = fastAddProperties(payload, prop[i], attributeConfig);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Or it can be an object.
|
||||
payload = fastAddProperties(payload, prop, attributeConfig);
|
||||
}
|
||||
|
||||
@@ -514,11 +514,7 @@ function addProperties(
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
if (enableAddPropertiesFastPath) {
|
||||
return fastAddProperties(updatePayload, props, validAttributes);
|
||||
} else {
|
||||
return diffProperties(updatePayload, emptyObject, props, validAttributes);
|
||||
}
|
||||
return diffProperties(updatePayload, emptyObject, props, validAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -538,11 +534,11 @@ export function create(
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
return addProperties(
|
||||
null, // updatePayload
|
||||
props,
|
||||
validAttributes,
|
||||
);
|
||||
if (enableAddPropertiesFastPath) {
|
||||
return fastAddProperties(null, props, validAttributes);
|
||||
} else {
|
||||
return addProperties(null, props, validAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
export function diff(
|
||||
|
||||
+449
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jest-environment node
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {diff, create} = require('../ReactNativeAttributePayloadFabric');
|
||||
|
||||
describe('ReactNativeAttributePayloadFabric.create', () => {
|
||||
it('should work with simple example', () => {
|
||||
expect(create({b: 2, c: 3}, {a: true, b: true})).toEqual({
|
||||
b: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with complex example', () => {
|
||||
const validAttributes = {
|
||||
style: {
|
||||
position: true,
|
||||
zIndex: true,
|
||||
flexGrow: true,
|
||||
flexShrink: true,
|
||||
flexDirection: true,
|
||||
overflow: true,
|
||||
backgroundColor: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
create(
|
||||
{
|
||||
style: [
|
||||
{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'scroll',
|
||||
},
|
||||
[
|
||||
{position: 'relative', zIndex: 2},
|
||||
{flexGrow: 0},
|
||||
{backgroundColor: 'red'},
|
||||
],
|
||||
],
|
||||
},
|
||||
validAttributes,
|
||||
),
|
||||
).toEqual({
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'scroll',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
backgroundColor: 'red',
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore fields that are set to undefined', () => {
|
||||
expect(create({}, {a: true})).toEqual(null);
|
||||
expect(create({a: undefined}, {a: true})).toEqual(null);
|
||||
expect(create({a: undefined, b: undefined}, {a: true, b: true})).toEqual(
|
||||
null,
|
||||
);
|
||||
expect(
|
||||
create({a: undefined, b: undefined, c: 1}, {a: true, b: true}),
|
||||
).toEqual(null);
|
||||
expect(
|
||||
create({a: undefined, b: undefined, c: 1}, {a: true, b: true, c: true}),
|
||||
).toEqual({c: 1});
|
||||
expect(
|
||||
create({a: 1, b: undefined, c: 2}, {a: true, b: true, c: true}),
|
||||
).toEqual({a: 1, c: 2});
|
||||
});
|
||||
|
||||
it('should ignore invalid fields', () => {
|
||||
expect(create({b: 2}, {})).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not use the diff attribute', () => {
|
||||
const diffA = jest.fn();
|
||||
expect(create({a: [2]}, {a: {diff: diffA}})).toEqual({a: [2]});
|
||||
expect(diffA).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should use the process attribute', () => {
|
||||
const processA = jest.fn(a => a + 1);
|
||||
expect(create({a: 2}, {a: {process: processA}})).toEqual({a: 3});
|
||||
expect(processA).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('should work with undefined styles', () => {
|
||||
expect(create({style: undefined}, {style: {b: true}})).toEqual(null);
|
||||
expect(create({style: {a: '#ffffff', b: 1}}, {style: {b: true}})).toEqual({
|
||||
b: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should flatten nested styles and predefined styles', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
expect(
|
||||
create({someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
|
||||
).toEqual({foo: 1, bar: 2});
|
||||
expect(create({}, validStyleAttribute)).toEqual(null);
|
||||
const barStyle = {
|
||||
bar: 3,
|
||||
};
|
||||
expect(
|
||||
create(
|
||||
{someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 2, bar: 3});
|
||||
});
|
||||
|
||||
it('should not flatten nested props if attribute config is a primitive or only has diff/process', () => {
|
||||
expect(create({a: {foo: 1, bar: 2}}, {a: true})).toEqual({
|
||||
a: {foo: 1, bar: 2},
|
||||
});
|
||||
expect(create({a: [{foo: 1}, {bar: 2}]}, {a: true})).toEqual({
|
||||
a: [{foo: 1}, {bar: 2}],
|
||||
});
|
||||
expect(create({a: {foo: 1, bar: 2}}, {a: {diff: a => a}})).toEqual({
|
||||
a: {foo: 1, bar: 2},
|
||||
});
|
||||
expect(
|
||||
create({a: [{foo: 1}, {bar: 2}]}, {a: {diff: a => a, process: a => a}}),
|
||||
).toEqual({a: [{foo: 1}, {bar: 2}]});
|
||||
});
|
||||
|
||||
it('handles attributes defined multiple times', () => {
|
||||
const validAttributes = {foo: true, style: {foo: true}};
|
||||
expect(create({foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(create({style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(create({style: {foo: 2}, foo: 4}, validAttributes)).toEqual({
|
||||
foo: 4,
|
||||
});
|
||||
expect(create({foo: 4, style: {foo: null}}, validAttributes)).toEqual({
|
||||
foo: null, // this should ideally be null.
|
||||
});
|
||||
expect(
|
||||
create({foo: 4, style: [{foo: null}, {foo: 5}]}, validAttributes),
|
||||
).toEqual({
|
||||
foo: 5,
|
||||
});
|
||||
});
|
||||
|
||||
// Function properties are just markers to native that events should be sent.
|
||||
it('should convert functions to booleans', () => {
|
||||
expect(
|
||||
create(
|
||||
{
|
||||
a: function () {
|
||||
return 9;
|
||||
},
|
||||
b: function () {
|
||||
return 3;
|
||||
},
|
||||
},
|
||||
{a: true, b: true},
|
||||
),
|
||||
).toEqual({a: true, b: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReactNativeAttributePayloadFabric.diff', () => {
|
||||
it('should work with simple example', () => {
|
||||
expect(diff({a: 1, c: 3}, {b: 2, c: 3}, {a: true, b: true})).toEqual({
|
||||
a: null,
|
||||
b: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip fields that are equal', () => {
|
||||
expect(
|
||||
diff(
|
||||
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
|
||||
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
|
||||
{a: true, b: true, c: true, d: true, e: true, f: true},
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should remove fields', () => {
|
||||
expect(diff({a: 1}, {}, {a: true})).toEqual({a: null});
|
||||
});
|
||||
|
||||
it('should remove fields that are set to undefined', () => {
|
||||
expect(diff({a: 1}, {a: undefined}, {a: true})).toEqual({a: null});
|
||||
});
|
||||
|
||||
it('should ignore invalid fields', () => {
|
||||
expect(diff({a: 1}, {b: 2}, {})).toEqual(null);
|
||||
});
|
||||
|
||||
// @gate !enableShallowPropDiffing
|
||||
it('should use the diff attribute', () => {
|
||||
const diffA = jest.fn((a, b) => true);
|
||||
const diffB = jest.fn((a, b) => false);
|
||||
expect(
|
||||
diff(
|
||||
{a: [1], b: [3]},
|
||||
{a: [2], b: [4]},
|
||||
{a: {diff: diffA}, b: {diff: diffB}},
|
||||
),
|
||||
).toEqual({a: [2]});
|
||||
expect(diffA).toBeCalledWith([1], [2]);
|
||||
expect(diffB).toBeCalledWith([3], [4]);
|
||||
});
|
||||
|
||||
it('should not use the diff attribute on addition/removal', () => {
|
||||
const diffA = jest.fn();
|
||||
const diffB = jest.fn();
|
||||
expect(
|
||||
diff({a: [1]}, {b: [2]}, {a: {diff: diffA}, b: {diff: diffB}}),
|
||||
).toEqual({a: null, b: [2]});
|
||||
expect(diffA).not.toBeCalled();
|
||||
expect(diffB).not.toBeCalled();
|
||||
});
|
||||
|
||||
// @gate !enableShallowPropDiffing
|
||||
it('should do deep diffs of Objects by default', () => {
|
||||
expect(
|
||||
diff(
|
||||
{a: [1], b: {k: [3, 4]}, c: {k: [4, 4]}},
|
||||
{a: [2], b: {k: [3, 4]}, c: {k: [4, 5]}},
|
||||
{a: true, b: true, c: true},
|
||||
),
|
||||
).toEqual({a: [2], c: {k: [4, 5]}});
|
||||
});
|
||||
|
||||
it('should work with undefined styles', () => {
|
||||
expect(
|
||||
diff(
|
||||
{style: {a: '#ffffff', b: 1}},
|
||||
{style: undefined},
|
||||
{style: {b: true}},
|
||||
),
|
||||
).toEqual({b: null});
|
||||
expect(
|
||||
diff(
|
||||
{style: undefined},
|
||||
{style: {a: '#ffffff', b: 1}},
|
||||
{style: {b: true}},
|
||||
),
|
||||
).toEqual({b: 1});
|
||||
expect(
|
||||
diff({style: undefined}, {style: undefined}, {style: {b: true}}),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should work with empty styles', () => {
|
||||
expect(diff({a: 1, c: 3}, {}, {a: true, b: true})).toEqual({a: null});
|
||||
expect(diff({}, {a: 1, c: 3}, {a: true, b: true})).toEqual({a: 1});
|
||||
expect(diff({}, {}, {a: true, b: true})).toEqual(null);
|
||||
});
|
||||
|
||||
it('should flatten nested styles and predefined styles', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
|
||||
expect(
|
||||
diff({}, {someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
|
||||
).toEqual({foo: 1, bar: 2});
|
||||
|
||||
expect(
|
||||
diff({someStyle: [{foo: 1}, {bar: 2}]}, {}, validStyleAttribute),
|
||||
).toEqual({foo: null, bar: null});
|
||||
|
||||
const barStyle = {
|
||||
bar: 3,
|
||||
};
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{},
|
||||
{someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 2, bar: 3});
|
||||
});
|
||||
|
||||
it('should reset a value to a previous if it is removed', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, {foo: 3}]},
|
||||
{someStyle: [{foo: 1}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 1, bar: 2});
|
||||
});
|
||||
|
||||
it('should not clear removed props if they are still in another slot', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{}, {foo: 3, bar: 2}]},
|
||||
{someStyle: [{foo: 3}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 3}); // this should ideally be null. heuristic tradeoff.
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{}, {foo: 3, bar: 2}]},
|
||||
{someStyle: [{foo: 1, bar: 1}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({bar: 2, foo: 1});
|
||||
});
|
||||
|
||||
it('should clear a prop if a later style is explicit null/undefined', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{}, {foo: 3, bar: 2}]},
|
||||
{someStyle: [{foo: 1}, {bar: 2, foo: null}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null});
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 3}, {foo: null, bar: 2}]},
|
||||
{someStyle: [{foo: null}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null});
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, {foo: null}]},
|
||||
{someStyle: [{foo: 2}, {foo: null}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null}); // this should ideally be null. heuristic.
|
||||
|
||||
// Test the same case with object equality because an early bailout doesn't
|
||||
// work in this case.
|
||||
const fooObj = {foo: 3};
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, fooObj]},
|
||||
{someStyle: [{foo: 2}, fooObj]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 3}); // this should ideally be null. heuristic.
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, {foo: 3}]},
|
||||
{someStyle: [{foo: 2}, {foo: undefined}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null}); // this should ideally be null. heuristic.
|
||||
});
|
||||
|
||||
it('handles attributes defined multiple times', () => {
|
||||
const validAttributes = {foo: true, style: {foo: true}};
|
||||
expect(diff({}, {foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(diff({foo: 4}, {style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(diff({style: {foo: 2}}, {foo: 4}, validAttributes)).toEqual({
|
||||
foo: 4,
|
||||
});
|
||||
});
|
||||
|
||||
// Function properties are just markers to native that events should be sent.
|
||||
it('should convert functions to booleans', () => {
|
||||
// Note that if the property changes from one function to another, we don't
|
||||
// need to send an update.
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
a: function () {
|
||||
return 1;
|
||||
},
|
||||
b: function () {
|
||||
return 2;
|
||||
},
|
||||
c: 3,
|
||||
},
|
||||
{
|
||||
b: function () {
|
||||
return 9;
|
||||
},
|
||||
c: function () {
|
||||
return 3;
|
||||
},
|
||||
},
|
||||
{a: true, b: true, c: true},
|
||||
),
|
||||
).toEqual({a: null, c: true});
|
||||
});
|
||||
|
||||
it('should skip changed functions', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
a: function () {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
a: function () {
|
||||
return 9;
|
||||
},
|
||||
},
|
||||
{a: true},
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
// @gate !enableShallowPropDiffing
|
||||
it('should skip deeply-nested changed functions', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
wrapper: {
|
||||
a: function () {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
wrapper: {
|
||||
a: function () {
|
||||
return 9;
|
||||
},
|
||||
},
|
||||
},
|
||||
{wrapper: true},
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
});
|
||||
+109
-104
@@ -1377,113 +1377,118 @@ describe('ResponderEventPlugin', () => {
|
||||
expect(ResponderEventPlugin._getResponder()).toBe(null);
|
||||
});
|
||||
|
||||
it('should determine the first common ancestor correctly', async () => {
|
||||
// This test was moved here from the ReactTreeTraversal test since only the
|
||||
// ResponderEventPlugin uses `getLowestCommonAncestor`
|
||||
const React = require('react');
|
||||
const ReactDOMClient = require('react-dom/client');
|
||||
const act = require('internal-test-utils').act;
|
||||
const getLowestCommonAncestor =
|
||||
require('react-native-renderer/src/legacy-events/ResponderEventPlugin').getLowestCommonAncestor;
|
||||
// This works by accident and will likely break in the future.
|
||||
const ReactDOMComponentTree = require('react-dom-bindings/src/client/ReactDOMComponentTree');
|
||||
it(
|
||||
'should determine the first common ancestor correctly',
|
||||
async () => {
|
||||
// This test was moved here from the ReactTreeTraversal test since only the
|
||||
// ResponderEventPlugin uses `getLowestCommonAncestor`
|
||||
const React = require('react');
|
||||
const ReactDOMClient = require('react-dom/client');
|
||||
const act = require('internal-test-utils').act;
|
||||
const getLowestCommonAncestor =
|
||||
require('react-native-renderer/src/legacy-events/ResponderEventPlugin').getLowestCommonAncestor;
|
||||
// This works by accident and will likely break in the future.
|
||||
const ReactDOMComponentTree = require('react-dom-bindings/src/client/ReactDOMComponentTree');
|
||||
|
||||
class ChildComponent extends React.Component {
|
||||
divRef = React.createRef();
|
||||
div1Ref = React.createRef();
|
||||
div2Ref = React.createRef();
|
||||
class ChildComponent extends React.Component {
|
||||
divRef = React.createRef();
|
||||
div1Ref = React.createRef();
|
||||
div2Ref = React.createRef();
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.divRef} id={this.props.id + '__DIV'}>
|
||||
<div ref={this.div1Ref} id={this.props.id + '__DIV_1'} />
|
||||
<div ref={this.div2Ref} id={this.props.id + '__DIV_2'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ParentComponent extends React.Component {
|
||||
pRef = React.createRef();
|
||||
p_P1Ref = React.createRef();
|
||||
p_P1_C1Ref = React.createRef();
|
||||
p_P1_C2Ref = React.createRef();
|
||||
p_OneOffRef = React.createRef();
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.pRef} id="P">
|
||||
<div ref={this.p_P1Ref} id="P_P1">
|
||||
<ChildComponent ref={this.p_P1_C1Ref} id="P_P1_C1" />
|
||||
<ChildComponent ref={this.p_P1_C2Ref} id="P_P1_C2" />
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.divRef} id={this.props.id + '__DIV'}>
|
||||
<div ref={this.div1Ref} id={this.props.id + '__DIV_1'} />
|
||||
<div ref={this.div2Ref} id={this.props.id + '__DIV_2'} />
|
||||
</div>
|
||||
<div ref={this.p_OneOffRef} id="P_OneOff" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ParentComponent extends React.Component {
|
||||
pRef = React.createRef();
|
||||
p_P1Ref = React.createRef();
|
||||
p_P1_C1Ref = React.createRef();
|
||||
p_P1_C2Ref = React.createRef();
|
||||
p_OneOffRef = React.createRef();
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.pRef} id="P">
|
||||
<div ref={this.p_P1Ref} id="P_P1">
|
||||
<ChildComponent ref={this.p_P1_C1Ref} id="P_P1_C1" />
|
||||
<ChildComponent ref={this.p_P1_C2Ref} id="P_P1_C2" />
|
||||
</div>
|
||||
<div ref={this.p_OneOffRef} id="P_OneOff" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
let parent;
|
||||
await act(() => {
|
||||
root.render(<ParentComponent ref={current => (parent = current)} />);
|
||||
});
|
||||
|
||||
const ancestors = [
|
||||
// Common ancestor with self is self.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
com: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
},
|
||||
// Common ancestor with self is self - even if topmost DOM.
|
||||
{
|
||||
one: parent.pRef.current,
|
||||
two: parent.pRef.current,
|
||||
com: parent.pRef.current,
|
||||
},
|
||||
// Siblings
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1_C1Ref.current.div2Ref.current,
|
||||
com: parent.p_P1_C1Ref.current.divRef.current,
|
||||
},
|
||||
// Common ancestor with parent is the parent.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1_C1Ref.current.divRef.current,
|
||||
com: parent.p_P1_C1Ref.current.divRef.current,
|
||||
},
|
||||
// Common ancestor with grandparent is the grandparent.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1Ref.current,
|
||||
com: parent.p_P1Ref.current,
|
||||
},
|
||||
// Grandparent across subcomponent boundaries.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1_C2Ref.current.div1Ref.current,
|
||||
com: parent.p_P1Ref.current,
|
||||
},
|
||||
// Something deep with something one-off.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_OneOffRef.current,
|
||||
com: parent.pRef.current,
|
||||
},
|
||||
];
|
||||
let i;
|
||||
for (i = 0; i < ancestors.length; i++) {
|
||||
const plan = ancestors[i];
|
||||
const firstCommon = getLowestCommonAncestor(
|
||||
ReactDOMComponentTree.getInstanceFromNode(plan.one),
|
||||
ReactDOMComponentTree.getInstanceFromNode(plan.two),
|
||||
);
|
||||
expect(firstCommon).toBe(
|
||||
ReactDOMComponentTree.getInstanceFromNode(plan.com),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
let parent;
|
||||
await act(() => {
|
||||
root.render(<ParentComponent ref={current => (parent = current)} />);
|
||||
});
|
||||
|
||||
const ancestors = [
|
||||
// Common ancestor with self is self.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
com: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
},
|
||||
// Common ancestor with self is self - even if topmost DOM.
|
||||
{
|
||||
one: parent.pRef.current,
|
||||
two: parent.pRef.current,
|
||||
com: parent.pRef.current,
|
||||
},
|
||||
// Siblings
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1_C1Ref.current.div2Ref.current,
|
||||
com: parent.p_P1_C1Ref.current.divRef.current,
|
||||
},
|
||||
// Common ancestor with parent is the parent.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1_C1Ref.current.divRef.current,
|
||||
com: parent.p_P1_C1Ref.current.divRef.current,
|
||||
},
|
||||
// Common ancestor with grandparent is the grandparent.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1Ref.current,
|
||||
com: parent.p_P1Ref.current,
|
||||
},
|
||||
// Grandparent across subcomponent boundaries.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_P1_C2Ref.current.div1Ref.current,
|
||||
com: parent.p_P1Ref.current,
|
||||
},
|
||||
// Something deep with something one-off.
|
||||
{
|
||||
one: parent.p_P1_C1Ref.current.div1Ref.current,
|
||||
two: parent.p_OneOffRef.current,
|
||||
com: parent.pRef.current,
|
||||
},
|
||||
];
|
||||
let i;
|
||||
for (i = 0; i < ancestors.length; i++) {
|
||||
const plan = ancestors[i];
|
||||
const firstCommon = getLowestCommonAncestor(
|
||||
ReactDOMComponentTree.getInstanceFromNode(plan.one),
|
||||
ReactDOMComponentTree.getInstanceFromNode(plan.two),
|
||||
);
|
||||
expect(firstCommon).toBe(
|
||||
ReactDOMComponentTree.getInstanceFromNode(plan.com),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
// TODO: this is a long running test, we should speed it up.
|
||||
60 * 1000,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,9 @@ type Destination = Array<Uint8Array>;
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
const ReactNoopFlightServer = ReactFlightServer({
|
||||
scheduleMicrotask(callback: () => void) {
|
||||
callback();
|
||||
},
|
||||
scheduleWork(callback: () => void) {
|
||||
callback();
|
||||
},
|
||||
|
||||
@@ -74,6 +74,9 @@ function write(destination: Destination, buffer: Uint8Array): void {
|
||||
}
|
||||
|
||||
const ReactNoopServer = ReactFizzServer({
|
||||
scheduleMicrotask(callback: () => void) {
|
||||
callback();
|
||||
},
|
||||
scheduleWork(callback: () => void) {
|
||||
callback();
|
||||
},
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ export function getCurrentParentStackInDev(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
function getCurrentFiberStackInDev(): string {
|
||||
function getCurrentFiberStackInDev(stack: Error): string {
|
||||
if (__DEV__) {
|
||||
if (current === null) {
|
||||
return '';
|
||||
@@ -54,7 +54,7 @@ function getCurrentFiberStackInDev(): string {
|
||||
// TODO: The above comment is not actually true. We might be
|
||||
// in a commit phase or preemptive set state callback.
|
||||
if (enableOwnerStacks) {
|
||||
return getOwnerStackByFiberInDev(current);
|
||||
return getOwnerStackByFiberInDev(current, stack);
|
||||
}
|
||||
return getStackByFiberInDevAndProd(current);
|
||||
}
|
||||
|
||||
+28
-14
@@ -1689,22 +1689,36 @@ function updateHostHoistable(
|
||||
renderLanes: Lanes,
|
||||
) {
|
||||
markRef(current, workInProgress);
|
||||
const currentProps = current === null ? null : current.memoizedProps;
|
||||
const resource = (workInProgress.memoizedState = getResource(
|
||||
workInProgress.type,
|
||||
currentProps,
|
||||
workInProgress.pendingProps,
|
||||
));
|
||||
|
||||
if (current === null) {
|
||||
if (!getIsHydrating() && resource === null) {
|
||||
// This is not a Resource Hoistable and we aren't hydrating so we construct the instance.
|
||||
workInProgress.stateNode = createHoistableInstance(
|
||||
workInProgress.type,
|
||||
workInProgress.pendingProps,
|
||||
getRootHostContainer(),
|
||||
workInProgress,
|
||||
);
|
||||
const resource = getResource(
|
||||
workInProgress.type,
|
||||
null,
|
||||
workInProgress.pendingProps,
|
||||
null,
|
||||
);
|
||||
if (resource) {
|
||||
workInProgress.memoizedState = resource;
|
||||
} else {
|
||||
if (!getIsHydrating()) {
|
||||
// This is not a Resource Hoistable and we aren't hydrating so we construct the instance.
|
||||
workInProgress.stateNode = createHoistableInstance(
|
||||
workInProgress.type,
|
||||
workInProgress.pendingProps,
|
||||
getRootHostContainer(),
|
||||
workInProgress,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get Resource may or may not return a resource. either way we stash the result
|
||||
// on memoized state.
|
||||
workInProgress.memoizedState = getResource(
|
||||
workInProgress.type,
|
||||
current.memoizedProps,
|
||||
workInProgress.pendingProps,
|
||||
current.memoizedState,
|
||||
);
|
||||
}
|
||||
|
||||
// Resources never have reconciler managed children. It is possible for
|
||||
|
||||
+15
-7
@@ -9,7 +9,7 @@
|
||||
|
||||
import type {LazyComponent} from 'react/src/ReactLazy';
|
||||
|
||||
import {setIsRendering} from './ReactCurrentFiber';
|
||||
import {isRendering, setIsRendering} from './ReactCurrentFiber';
|
||||
|
||||
// These indirections exists so we can exclude its stack frame in DEV (and anything below it).
|
||||
// TODO: Consider marking the whole bundle instead of these boundaries.
|
||||
@@ -20,10 +20,14 @@ export function callComponentInDEV<Props, Arg, R>(
|
||||
props: Props,
|
||||
secondArg: Arg,
|
||||
): R {
|
||||
const wasRendering = isRendering;
|
||||
setIsRendering(true);
|
||||
const result = Component(props, secondArg);
|
||||
setIsRendering(false);
|
||||
return result;
|
||||
try {
|
||||
const result = Component(props, secondArg);
|
||||
return result;
|
||||
} finally {
|
||||
setIsRendering(wasRendering);
|
||||
}
|
||||
}
|
||||
|
||||
interface ClassInstance<R> {
|
||||
@@ -32,10 +36,14 @@ interface ClassInstance<R> {
|
||||
|
||||
/** @noinline */
|
||||
export function callRenderInDEV<R>(instance: ClassInstance<R>): R {
|
||||
const wasRendering = isRendering;
|
||||
setIsRendering(true);
|
||||
const result = instance.render();
|
||||
setIsRendering(false);
|
||||
return result;
|
||||
try {
|
||||
const result = instance.render();
|
||||
return result;
|
||||
} finally {
|
||||
setIsRendering(wasRendering);
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinline */
|
||||
|
||||
+19
-19
@@ -1052,7 +1052,6 @@ function completeWork(
|
||||
return null;
|
||||
} else {
|
||||
// This is a Hoistable Instance
|
||||
|
||||
// This must come at the very end of the complete phase.
|
||||
bubbleProperties(workInProgress);
|
||||
preloadInstanceAndSuspendIfNeeded(
|
||||
@@ -1064,21 +1063,18 @@ function completeWork(
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// We are updating.
|
||||
const currentResource = current.memoizedState;
|
||||
if (nextResource !== currentResource) {
|
||||
// We are transitioning to, from, or between Hoistable Resources
|
||||
// and require an update
|
||||
markUpdate(workInProgress);
|
||||
}
|
||||
if (nextResource !== null) {
|
||||
// This is a Hoistable Resource
|
||||
// This must come at the very end of the complete phase.
|
||||
|
||||
bubbleProperties(workInProgress);
|
||||
if (nextResource === currentResource) {
|
||||
workInProgress.flags &= ~MaySuspendCommit;
|
||||
} else {
|
||||
// This is an update.
|
||||
if (nextResource) {
|
||||
// This is a Resource
|
||||
if (nextResource !== current.memoizedState) {
|
||||
// we have a new Resource. we need to update
|
||||
markUpdate(workInProgress);
|
||||
// This must come at the very end of the complete phase.
|
||||
bubbleProperties(workInProgress);
|
||||
// This must come at the very end of the complete phase, because it might
|
||||
// throw to suspend, and if the resource immediately loads, the work loop
|
||||
// will resume rendering as if the work-in-progress completed. So it must
|
||||
// fully complete.
|
||||
preloadResourceAndSuspendIfNeeded(
|
||||
workInProgress,
|
||||
nextResource,
|
||||
@@ -1086,10 +1082,15 @@ function completeWork(
|
||||
newProps,
|
||||
renderLanes,
|
||||
);
|
||||
return null;
|
||||
} else {
|
||||
// This must come at the very end of the complete phase.
|
||||
bubbleProperties(workInProgress);
|
||||
workInProgress.flags &= ~MaySuspendCommit;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
// This is a Hoistable Instance
|
||||
// This is an Instance
|
||||
// We may have props to update on the Hoistable instance.
|
||||
if (supportsMutation) {
|
||||
const oldProps = current.memoizedProps;
|
||||
@@ -1107,7 +1108,6 @@ function completeWork(
|
||||
renderLanes,
|
||||
);
|
||||
}
|
||||
|
||||
// This must come at the very end of the complete phase.
|
||||
bubbleProperties(workInProgress);
|
||||
preloadInstanceAndSuspendIfNeeded(
|
||||
|
||||
+19
-3
@@ -90,13 +90,27 @@ function describeFunctionComponentFrameWithoutLineNumber(fn: Function): string {
|
||||
return name ? describeBuiltInComponentFrame(name) : '';
|
||||
}
|
||||
|
||||
export function getOwnerStackByFiberInDev(workInProgress: Fiber): string {
|
||||
export function getOwnerStackByFiberInDev(
|
||||
workInProgress: Fiber,
|
||||
topStack: null | Error,
|
||||
): string {
|
||||
if (!enableOwnerStacks || !__DEV__) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
let info = '';
|
||||
|
||||
if (topStack) {
|
||||
// Prefix with a filtered version of the currently executing
|
||||
// stack. This information will be available in the native
|
||||
// stack regardless but it's hidden since we're reprinting
|
||||
// the stack on top of it.
|
||||
const formattedTopStack = formatOwnerStack(topStack);
|
||||
if (formattedTopStack !== '') {
|
||||
info += '\n' + formattedTopStack;
|
||||
}
|
||||
}
|
||||
|
||||
if (workInProgress.tag === HostText) {
|
||||
// Text nodes never have an owner/stack because they're not created through JSX.
|
||||
// We use the parent since text nodes are always created through a host parent.
|
||||
@@ -125,14 +139,16 @@ export function getOwnerStackByFiberInDev(workInProgress: Fiber): string {
|
||||
case FunctionComponent:
|
||||
case SimpleMemoComponent:
|
||||
case ClassComponent:
|
||||
if (!workInProgress._debugOwner) {
|
||||
if (!workInProgress._debugOwner && info === '') {
|
||||
// Only if we have no other data about the callsite do we add
|
||||
// the component name as the single stack frame.
|
||||
info += describeFunctionComponentFrameWithoutLineNumber(
|
||||
workInProgress.type,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ForwardRef:
|
||||
if (!workInProgress._debugOwner) {
|
||||
if (!workInProgress._debugOwner && info === '') {
|
||||
info += describeFunctionComponentFrameWithoutLineNumber(
|
||||
workInProgress.type.render,
|
||||
);
|
||||
|
||||
+229
-129
@@ -1083,20 +1083,49 @@ function useThenable<T>(thenable: Thenable<T>): T {
|
||||
thenableState = createThenableState();
|
||||
}
|
||||
const result = trackUsedThenable(thenableState, thenable, index);
|
||||
if (
|
||||
currentlyRenderingFiber.alternate === null &&
|
||||
(workInProgressHook === null
|
||||
? currentlyRenderingFiber.memoizedState === null
|
||||
: workInProgressHook.next === null)
|
||||
) {
|
||||
// Initial render, and either this is the first time the component is
|
||||
// called, or there were no Hooks called after this use() the previous
|
||||
// time (perhaps because it threw). Subsequent Hook calls should use the
|
||||
// mount dispatcher.
|
||||
|
||||
// When something suspends with `use`, we replay the component with the
|
||||
// "re-render" dispatcher instead of the "mount" or "update" dispatcher.
|
||||
//
|
||||
// But if there are additional hooks that occur after the `use` invocation
|
||||
// that suspended, they wouldn't have been processed during the previous
|
||||
// attempt. So after we invoke `use` again, we may need to switch from the
|
||||
// "re-render" dispatcher back to the "mount" or "update" dispatcher. That's
|
||||
// what the following logic accounts for.
|
||||
//
|
||||
// TODO: Theoretically this logic only needs to go into the rerender
|
||||
// dispatcher. Could optimize, but probably not be worth it.
|
||||
|
||||
// This is the same logic as in updateWorkInProgressHook.
|
||||
const workInProgressFiber = currentlyRenderingFiber;
|
||||
const nextWorkInProgressHook =
|
||||
workInProgressHook === null
|
||||
? // We're at the beginning of the list, so read from the first hook from
|
||||
// the fiber.
|
||||
workInProgressFiber.memoizedState
|
||||
: workInProgressHook.next;
|
||||
|
||||
if (nextWorkInProgressHook !== null) {
|
||||
// There are still hooks remaining from the previous attempt.
|
||||
} else {
|
||||
// There are no remaining hooks from the previous attempt. We're no longer
|
||||
// in "re-render" mode. Switch to the normal mount or update dispatcher.
|
||||
//
|
||||
// This is the same as the logic in renderWithHooks, except we don't bother
|
||||
// to track the hook types debug information in this case (sufficient to
|
||||
// only do that when nothing suspends).
|
||||
const currentFiber = workInProgressFiber.alternate;
|
||||
if (__DEV__) {
|
||||
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
|
||||
if (currentFiber !== null && currentFiber.memoizedState !== null) {
|
||||
ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
|
||||
} else {
|
||||
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
|
||||
}
|
||||
} else {
|
||||
ReactSharedInternals.H = HooksDispatcherOnMount;
|
||||
ReactSharedInternals.H =
|
||||
currentFiber === null || currentFiber.memoizedState === null
|
||||
? HooksDispatcherOnMount
|
||||
: HooksDispatcherOnUpdate;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -1965,7 +1994,9 @@ type ActionStateQueue<S, P> = {
|
||||
dispatch: Dispatch<P>,
|
||||
// This is the most recent action function that was rendered. It's updated
|
||||
// during the commit phase.
|
||||
action: (Awaited<S>, P) => S,
|
||||
// If it's null, it means the action queue errored and subsequent actions
|
||||
// should not run.
|
||||
action: ((Awaited<S>, P) => S) | null,
|
||||
// This is a circular linked list of pending action payloads. It incudes the
|
||||
// action that is currently running.
|
||||
pending: ActionStateQueueNode<S, P> | null,
|
||||
@@ -1977,65 +2008,93 @@ type ActionStateQueueNode<S, P> = {
|
||||
action: (Awaited<S>, P) => S,
|
||||
// This is never null because it's part of a circular linked list.
|
||||
next: ActionStateQueueNode<S, P>,
|
||||
|
||||
// Whether or not the action was dispatched as part of a transition. We use
|
||||
// this to restore the transition context when the queued action is run. Once
|
||||
// we're able to track parallel async actions, this should be updated to
|
||||
// represent the specific transition instance the action is associated with.
|
||||
isTransition: boolean,
|
||||
|
||||
// Implements the Thenable interface. We use it to suspend until the action
|
||||
// finishes.
|
||||
then: (listener: () => void) => void,
|
||||
status: 'pending' | 'rejected' | 'fulfilled',
|
||||
value: any,
|
||||
reason: any,
|
||||
listeners: Array<() => void>,
|
||||
};
|
||||
|
||||
function dispatchActionState<S, P>(
|
||||
fiber: Fiber,
|
||||
actionQueue: ActionStateQueue<S, P>,
|
||||
setPendingState: boolean => void,
|
||||
setState: Dispatch<S | Awaited<S>>,
|
||||
setState: Dispatch<ActionStateQueueNode<S, P>>,
|
||||
payload: P,
|
||||
): void {
|
||||
if (isRenderPhaseUpdate(fiber)) {
|
||||
throw new Error('Cannot update form state while rendering.');
|
||||
}
|
||||
|
||||
const currentAction = actionQueue.action;
|
||||
if (currentAction === null) {
|
||||
// An earlier action errored. Subsequent actions should not run.
|
||||
return;
|
||||
}
|
||||
|
||||
const actionNode: ActionStateQueueNode<S, P> = {
|
||||
payload,
|
||||
action: currentAction,
|
||||
next: (null: any), // circular
|
||||
|
||||
isTransition: true,
|
||||
|
||||
status: 'pending',
|
||||
value: null,
|
||||
reason: null,
|
||||
listeners: [],
|
||||
then(listener) {
|
||||
// We know the only thing that subscribes to these promises is `use` so
|
||||
// this implementation is simpler than a generic thenable. E.g. we don't
|
||||
// bother to check if the thenable is still pending because `use` already
|
||||
// does that.
|
||||
actionNode.listeners.push(listener);
|
||||
},
|
||||
};
|
||||
|
||||
// Check if we're inside a transition. If so, we'll need to restore the
|
||||
// transition context when the action is run.
|
||||
const prevTransition = ReactSharedInternals.T;
|
||||
if (prevTransition !== null) {
|
||||
// Optimistically update the pending state, similar to useTransition.
|
||||
// This will be reverted automatically when all actions are finished.
|
||||
setPendingState(true);
|
||||
// `actionNode` is a thenable that resolves to the return value of
|
||||
// the action.
|
||||
setState(actionNode);
|
||||
} else {
|
||||
// This is not a transition.
|
||||
actionNode.isTransition = false;
|
||||
setState(actionNode);
|
||||
}
|
||||
|
||||
const last = actionQueue.pending;
|
||||
if (last === null) {
|
||||
// There are no pending actions; this is the first one. We can run
|
||||
// it immediately.
|
||||
const newLast: ActionStateQueueNode<S, P> = {
|
||||
payload,
|
||||
action: actionQueue.action,
|
||||
next: (null: any), // circular
|
||||
};
|
||||
newLast.next = actionQueue.pending = newLast;
|
||||
|
||||
runActionStateAction(
|
||||
actionQueue,
|
||||
(setPendingState: any),
|
||||
(setState: any),
|
||||
newLast,
|
||||
);
|
||||
actionNode.next = actionQueue.pending = actionNode;
|
||||
runActionStateAction(actionQueue, actionNode);
|
||||
} else {
|
||||
// There's already an action running. Add to the queue.
|
||||
const first = last.next;
|
||||
const newLast: ActionStateQueueNode<S, P> = {
|
||||
payload,
|
||||
action: actionQueue.action,
|
||||
next: first,
|
||||
};
|
||||
actionQueue.pending = last.next = newLast;
|
||||
actionNode.next = first;
|
||||
actionQueue.pending = last.next = actionNode;
|
||||
}
|
||||
}
|
||||
|
||||
function runActionStateAction<S, P>(
|
||||
actionQueue: ActionStateQueue<S, P>,
|
||||
setPendingState: boolean => void,
|
||||
setState: Dispatch<S | Awaited<S>>,
|
||||
node: ActionStateQueueNode<S, P>,
|
||||
) {
|
||||
// This is a fork of startTransition
|
||||
const prevTransition = ReactSharedInternals.T;
|
||||
const currentTransition: BatchConfigTransition = {};
|
||||
ReactSharedInternals.T = currentTransition;
|
||||
if (__DEV__) {
|
||||
ReactSharedInternals.T._updatedFibers = new Set();
|
||||
}
|
||||
|
||||
// Optimistically update the pending state, similar to useTransition.
|
||||
// This will be reverted automatically when all actions are finished.
|
||||
setPendingState(true);
|
||||
|
||||
// `node.action` represents the action function at the time it was dispatched.
|
||||
// If this action was queued, it might be stale, i.e. it's not necessarily the
|
||||
// most current implementation of the action, stored on `actionQueue`. This is
|
||||
@@ -2045,93 +2104,106 @@ function runActionStateAction<S, P>(
|
||||
const action = node.action;
|
||||
const payload = node.payload;
|
||||
const prevState = actionQueue.state;
|
||||
try {
|
||||
const returnValue = action(prevState, payload);
|
||||
const onStartTransitionFinish = ReactSharedInternals.S;
|
||||
if (onStartTransitionFinish !== null) {
|
||||
onStartTransitionFinish(currentTransition, returnValue);
|
||||
}
|
||||
if (
|
||||
returnValue !== null &&
|
||||
typeof returnValue === 'object' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof returnValue.then === 'function'
|
||||
) {
|
||||
const thenable = ((returnValue: any): Thenable<Awaited<S>>);
|
||||
|
||||
// Attach a listener to read the return state of the action. As soon as
|
||||
// this resolves, we can run the next action in the sequence.
|
||||
thenable.then(
|
||||
(nextState: Awaited<S>) => {
|
||||
actionQueue.state = nextState;
|
||||
finishRunningActionStateAction(
|
||||
actionQueue,
|
||||
(setPendingState: any),
|
||||
(setState: any),
|
||||
);
|
||||
},
|
||||
() =>
|
||||
finishRunningActionStateAction(
|
||||
actionQueue,
|
||||
(setPendingState: any),
|
||||
(setState: any),
|
||||
),
|
||||
);
|
||||
|
||||
setState((thenable: any));
|
||||
} else {
|
||||
setState((returnValue: any));
|
||||
|
||||
const nextState = ((returnValue: any): Awaited<S>);
|
||||
actionQueue.state = nextState;
|
||||
finishRunningActionStateAction(
|
||||
actionQueue,
|
||||
(setPendingState: any),
|
||||
(setState: any),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// This is a trick to get the `useActionState` hook to rethrow the error.
|
||||
// When it unwraps the thenable with the `use` algorithm, the error
|
||||
// will be thrown.
|
||||
const rejectedThenable: S = ({
|
||||
then() {},
|
||||
status: 'rejected',
|
||||
reason: error,
|
||||
// $FlowFixMe: Not sure why this doesn't work
|
||||
}: RejectedThenable<Awaited<S>>);
|
||||
setState(rejectedThenable);
|
||||
finishRunningActionStateAction(
|
||||
actionQueue,
|
||||
(setPendingState: any),
|
||||
(setState: any),
|
||||
);
|
||||
} finally {
|
||||
ReactSharedInternals.T = prevTransition;
|
||||
if (node.isTransition) {
|
||||
// The original dispatch was part of a transition. We restore its
|
||||
// transition context here.
|
||||
|
||||
// This is a fork of startTransition
|
||||
const prevTransition = ReactSharedInternals.T;
|
||||
const currentTransition: BatchConfigTransition = {};
|
||||
ReactSharedInternals.T = currentTransition;
|
||||
if (__DEV__) {
|
||||
if (prevTransition === null && currentTransition._updatedFibers) {
|
||||
const updatedFibersCount = currentTransition._updatedFibers.size;
|
||||
currentTransition._updatedFibers.clear();
|
||||
if (updatedFibersCount > 10) {
|
||||
console.warn(
|
||||
'Detected a large number of updates inside startTransition. ' +
|
||||
'If this is due to a subscription please re-write it to use React provided hooks. ' +
|
||||
'Otherwise concurrent mode guarantees are off the table.',
|
||||
);
|
||||
ReactSharedInternals.T._updatedFibers = new Set();
|
||||
}
|
||||
try {
|
||||
const returnValue = action(prevState, payload);
|
||||
const onStartTransitionFinish = ReactSharedInternals.S;
|
||||
if (onStartTransitionFinish !== null) {
|
||||
onStartTransitionFinish(currentTransition, returnValue);
|
||||
}
|
||||
handleActionReturnValue(actionQueue, node, returnValue);
|
||||
} catch (error) {
|
||||
onActionError(actionQueue, node, error);
|
||||
} finally {
|
||||
ReactSharedInternals.T = prevTransition;
|
||||
|
||||
if (__DEV__) {
|
||||
if (prevTransition === null && currentTransition._updatedFibers) {
|
||||
const updatedFibersCount = currentTransition._updatedFibers.size;
|
||||
currentTransition._updatedFibers.clear();
|
||||
if (updatedFibersCount > 10) {
|
||||
console.warn(
|
||||
'Detected a large number of updates inside startTransition. ' +
|
||||
'If this is due to a subscription please re-write it to use React provided hooks. ' +
|
||||
'Otherwise concurrent mode guarantees are off the table.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The original dispatch was not part of a transition.
|
||||
try {
|
||||
const returnValue = action(prevState, payload);
|
||||
handleActionReturnValue(actionQueue, node, returnValue);
|
||||
} catch (error) {
|
||||
onActionError(actionQueue, node, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finishRunningActionStateAction<S, P>(
|
||||
function handleActionReturnValue<S, P>(
|
||||
actionQueue: ActionStateQueue<S, P>,
|
||||
setPendingState: Dispatch<S | Awaited<S>>,
|
||||
setState: Dispatch<S | Awaited<S>>,
|
||||
node: ActionStateQueueNode<S, P>,
|
||||
returnValue: mixed,
|
||||
) {
|
||||
// The action finished running. Pop it from the queue and run the next pending
|
||||
// action, if there are any.
|
||||
if (
|
||||
returnValue !== null &&
|
||||
typeof returnValue === 'object' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof returnValue.then === 'function'
|
||||
) {
|
||||
const thenable = ((returnValue: any): Thenable<Awaited<S>>);
|
||||
// Attach a listener to read the return state of the action. As soon as
|
||||
// this resolves, we can run the next action in the sequence.
|
||||
thenable.then(
|
||||
(nextState: Awaited<S>) => {
|
||||
onActionSuccess(actionQueue, node, nextState);
|
||||
},
|
||||
(error: mixed) => onActionError(actionQueue, node, error),
|
||||
);
|
||||
|
||||
if (__DEV__) {
|
||||
if (!node.isTransition) {
|
||||
console.error(
|
||||
'An async function was passed to useActionState, but it was ' +
|
||||
'dispatched outside of an action context. This is likely not ' +
|
||||
'what you intended. Either pass the dispatch function to an ' +
|
||||
'`action` prop, or dispatch manually inside `startTransition`',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const nextState = ((returnValue: any): Awaited<S>);
|
||||
onActionSuccess(actionQueue, node, nextState);
|
||||
}
|
||||
}
|
||||
|
||||
function onActionSuccess<S, P>(
|
||||
actionQueue: ActionStateQueue<S, P>,
|
||||
actionNode: ActionStateQueueNode<S, P>,
|
||||
nextState: Awaited<S>,
|
||||
) {
|
||||
// The action finished running.
|
||||
actionNode.status = 'fulfilled';
|
||||
actionNode.value = nextState;
|
||||
notifyActionListeners(actionNode);
|
||||
|
||||
actionQueue.state = nextState;
|
||||
|
||||
// Pop the action from the queue and run the next pending action, if there
|
||||
// are any.
|
||||
const last = actionQueue.pending;
|
||||
if (last !== null) {
|
||||
const first = last.next;
|
||||
@@ -2144,16 +2216,44 @@ function finishRunningActionStateAction<S, P>(
|
||||
last.next = next;
|
||||
|
||||
// Run the next action.
|
||||
runActionStateAction(
|
||||
actionQueue,
|
||||
(setPendingState: any),
|
||||
(setState: any),
|
||||
next,
|
||||
);
|
||||
runActionStateAction(actionQueue, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onActionError<S, P>(
|
||||
actionQueue: ActionStateQueue<S, P>,
|
||||
actionNode: ActionStateQueueNode<S, P>,
|
||||
error: mixed,
|
||||
) {
|
||||
// Mark all the following actions as rejected.
|
||||
const last = actionQueue.pending;
|
||||
actionQueue.pending = null;
|
||||
if (last !== null) {
|
||||
const first = last.next;
|
||||
do {
|
||||
actionNode.status = 'rejected';
|
||||
actionNode.reason = error;
|
||||
notifyActionListeners(actionNode);
|
||||
actionNode = actionNode.next;
|
||||
} while (actionNode !== first);
|
||||
}
|
||||
|
||||
// Prevent subsequent actions from being dispatched.
|
||||
actionQueue.action = null;
|
||||
}
|
||||
|
||||
function notifyActionListeners<S, P>(actionNode: ActionStateQueueNode<S, P>) {
|
||||
// Notify React that the action has finished.
|
||||
const listeners = actionNode.listeners;
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
// This is always a React internal listener, so we don't need to worry
|
||||
// about it throwing.
|
||||
const listener = listeners[i];
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function actionStateReducer<S>(oldState: S, newState: S): S {
|
||||
return newState;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,11 @@ function filterDebugStack(error: Error): string {
|
||||
if (lastFrameIdx !== -1) {
|
||||
// Cut off everything after our "callComponent" slot since it'll be Fiber internals.
|
||||
frames.length = lastFrameIdx;
|
||||
} else {
|
||||
// We didn't find any internal callsite out to user space.
|
||||
// This means that this was called outside an owner or the owner is fully internal.
|
||||
// To keep things light we exclude the entire trace in this case.
|
||||
return '';
|
||||
}
|
||||
return frames.filter(isNotExternal).join('\n');
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('Activity', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate www && !disableLegacyMode
|
||||
// @gate enableLegacyHidden && !disableLegacyMode
|
||||
it('does not defer in legacy mode', async () => {
|
||||
let setState;
|
||||
function Foo() {
|
||||
@@ -163,7 +163,7 @@ describe('Activity', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableLegacyHidden
|
||||
it('does defer in concurrent mode', async () => {
|
||||
let setState;
|
||||
function Foo() {
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('Activity Suspense', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableLegacyHidden
|
||||
test('LegacyHidden does not handle suspense', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('Activity Suspense', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
// @gate enableActivity
|
||||
test("suspending inside currently hidden tree that's switching to visible", async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
@@ -319,7 +319,7 @@ describe('Activity Suspense', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
// @gate enableActivity
|
||||
test('update that suspends inside hidden tree', async () => {
|
||||
let setText;
|
||||
function Child() {
|
||||
@@ -352,7 +352,7 @@ describe('Activity Suspense', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
// @gate enableActivity
|
||||
test('updates at multiple priorities that suspend inside hidden tree', async () => {
|
||||
let setText;
|
||||
let setStep;
|
||||
|
||||
@@ -550,7 +550,7 @@ describe('ReactLazyContextPropagation', () => {
|
||||
expect(root).toMatchRenderedOutput('BB');
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableLegacyCache && enableLegacyHidden
|
||||
test('context is propagated through offscreen trees', async () => {
|
||||
const LegacyHidden = React.unstable_LegacyHidden;
|
||||
|
||||
@@ -596,7 +596,7 @@ describe('ReactLazyContextPropagation', () => {
|
||||
expect(root).toMatchRenderedOutput('BB');
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableLegacyCache && enableLegacyHidden
|
||||
test('multiple contexts are propagated across through offscreen trees', async () => {
|
||||
// Same as previous test, but with multiple context providers
|
||||
const LegacyHidden = React.unstable_LegacyHidden;
|
||||
@@ -822,7 +822,7 @@ describe('ReactLazyContextPropagation', () => {
|
||||
expect(root).toMatchRenderedOutput('BB');
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableLegacyCache && enableLegacyHidden
|
||||
test('nested bailouts through offscreen trees', async () => {
|
||||
// Lazy context propagation will stop propagating when it hits the first
|
||||
// match. If we bail out again inside that tree, we must resume propagating.
|
||||
|
||||
@@ -1618,8 +1618,7 @@ describe('ReactHooks', () => {
|
||||
' Previous render Next render\n' +
|
||||
' ------------------------------------------------------\n' +
|
||||
`1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameB)}\n` +
|
||||
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
|
||||
' in App (at **)',
|
||||
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
|
||||
]);
|
||||
|
||||
// further warnings for this component are silenced
|
||||
@@ -1671,8 +1670,7 @@ describe('ReactHooks', () => {
|
||||
' ------------------------------------------------------\n' +
|
||||
`1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameA)}\n` +
|
||||
`2. undefined use${hookNameB}\n` +
|
||||
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
|
||||
' in App (at **)',
|
||||
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1758,8 +1756,7 @@ describe('ReactHooks', () => {
|
||||
'ImperativeHandle',
|
||||
'Memo',
|
||||
)}\n` +
|
||||
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
|
||||
' in App (at **)',
|
||||
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
|
||||
]);
|
||||
|
||||
// further warnings for this component are silenced
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('ReactIncremental', () => {
|
||||
expect(inst.state).toEqual({text: 'bar', text2: 'baz'});
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableLegacyHidden
|
||||
it('can deprioritize unfinished work and resume it later', async () => {
|
||||
function Bar(props) {
|
||||
Scheduler.log('Bar');
|
||||
@@ -279,7 +279,7 @@ describe('ReactIncremental', () => {
|
||||
await waitForAll(['Middle', 'Middle']);
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableLegacyHidden
|
||||
it('can deprioritize a tree from without dropping work', async () => {
|
||||
function Bar(props) {
|
||||
Scheduler.log('Bar');
|
||||
@@ -1864,8 +1864,7 @@ describe('ReactIncremental', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate !disableLegacyContext
|
||||
// @gate enableLegacyHidden && !disableLegacyContext
|
||||
it('provides context when reusing work', async () => {
|
||||
class Intl extends React.Component {
|
||||
static childContextTypes = {
|
||||
|
||||
+1
-1
@@ -289,7 +289,7 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableLegacyHidden
|
||||
it('does not include offscreen work when retrying after an error', async () => {
|
||||
function App(props) {
|
||||
if (props.isBroken) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user