mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge 96d9825588 into sapling-pr-archive-josephsavona
This commit is contained in:
@@ -6,52 +6,51 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {parseConfigPragmaAsString} from 'babel-plugin-react-compiler';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import parserBabel from 'prettier/plugins/babel';
|
||||
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {useState, useEffect} from 'react';
|
||||
import {useState} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore} from '../StoreContext';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {
|
||||
generateOverridePragmaFromConfig,
|
||||
updateSourceWithOverridePragma,
|
||||
} from '../../lib/configUtils';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor(): JSX.Element {
|
||||
const [, setMonaco] = useState<Monaco | null>(null);
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
|
||||
// Parse string-based override config from pragma comment and format it
|
||||
const [configJavaScript, setConfigJavaScript] = useState('');
|
||||
const handleChange: (value: string | undefined) => void = async value => {
|
||||
if (value === undefined) return;
|
||||
|
||||
useEffect(() => {
|
||||
const pragma = store.source.substring(0, store.source.indexOf('\n'));
|
||||
const configString = `(${parseConfigPragmaAsString(pragma)})`;
|
||||
try {
|
||||
const newPragma = await generateOverridePragmaFromConfig(value);
|
||||
const updatedSource = updateSourceWithOverridePragma(
|
||||
store.source,
|
||||
newPragma,
|
||||
);
|
||||
|
||||
prettier
|
||||
.format(configString, {
|
||||
semi: true,
|
||||
parser: 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
})
|
||||
.then(formatted => {
|
||||
setConfigJavaScript(formatted);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error formatting config:', error);
|
||||
setConfigJavaScript('({})'); // Return empty object if not valid for now
|
||||
//TODO: Add validation and error handling for config
|
||||
// Update the store with both the new config and updated source
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: updatedSource,
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
console.log('Config:', configString);
|
||||
}, [store.source]);
|
||||
|
||||
const handleChange: (value: string | undefined) => void = value => {
|
||||
if (!value) return;
|
||||
|
||||
// TODO: Implement sync logic to update pragma comments in the source
|
||||
console.log('Config changed:', value);
|
||||
} catch (_) {
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: store.source,
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMount: (
|
||||
@@ -81,12 +80,11 @@ export default function ConfigEditor(): JSX.Element {
|
||||
<MonacoEditor
|
||||
path={'config.js'}
|
||||
language={'javascript'}
|
||||
value={configJavaScript}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
readOnly: true,
|
||||
lineNumbers: 'off',
|
||||
folding: false,
|
||||
renderLineHighlight: 'none',
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
import {useSearchParams} from 'next/navigation';
|
||||
import {parseAndFormatConfig} from '../../lib/configUtils';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -315,9 +316,17 @@ export default function Editor(): JSX.Element {
|
||||
});
|
||||
mountStore = defaultStore;
|
||||
}
|
||||
dispatchStore({
|
||||
type: 'setStore',
|
||||
payload: {store: mountStore},
|
||||
|
||||
parseAndFormatConfig(mountStore.source).then(config => {
|
||||
dispatchStore({
|
||||
type: 'setStore',
|
||||
payload: {
|
||||
store: {
|
||||
...mountStore,
|
||||
config,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
|
||||
import React$Types from '../../node_modules/@types/react/index.d.ts';
|
||||
import {parseAndFormatConfig} from '../../lib/configUtils.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
@@ -79,13 +80,17 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
});
|
||||
}, [monaco, language]);
|
||||
|
||||
const handleChange: (value: string | undefined) => void = value => {
|
||||
const handleChange: (value: string | undefined) => void = async value => {
|
||||
if (!value) return;
|
||||
|
||||
// Parse and format the config
|
||||
const config = await parseAndFormatConfig(value);
|
||||
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: value,
|
||||
config,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ type ReducerAction =
|
||||
type: 'updateFile';
|
||||
payload: {
|
||||
source: string;
|
||||
config?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -66,10 +67,11 @@ function storeReducer(store: Store, action: ReducerAction): Store {
|
||||
return newStore;
|
||||
}
|
||||
case 'updateFile': {
|
||||
const {source} = action.payload;
|
||||
const {source, config} = action.payload;
|
||||
const newStore = {
|
||||
...store,
|
||||
source,
|
||||
config,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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 parserBabel from 'prettier/plugins/babel';
|
||||
import prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
|
||||
|
||||
/**
|
||||
* Parse config from pragma and format it with prettier
|
||||
*/
|
||||
export async function parseAndFormatConfig(source: string): Promise<string> {
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
let configString = parseConfigPragmaAsString(pragma);
|
||||
if (configString !== '') {
|
||||
configString = `(${configString})`;
|
||||
}
|
||||
|
||||
try {
|
||||
const formatted = await prettier.format(configString, {
|
||||
semi: true,
|
||||
parser: 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
return formatted;
|
||||
} catch (error) {
|
||||
console.error('Error formatting config:', error);
|
||||
return ''; // Return empty string if not valid for now
|
||||
}
|
||||
}
|
||||
|
||||
function extractCurlyBracesContent(input: string): string {
|
||||
const startIndex = input.indexOf('{');
|
||||
const endIndex = input.lastIndexOf('}');
|
||||
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
||||
throw new Error('No outer curly braces found in input');
|
||||
}
|
||||
return input.slice(startIndex, endIndex + 1);
|
||||
}
|
||||
|
||||
function cleanContent(content: string): string {
|
||||
return content
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a the override pragma comment from a formatted config object string
|
||||
*/
|
||||
export async function generateOverridePragmaFromConfig(
|
||||
formattedConfigString: string,
|
||||
): Promise<string> {
|
||||
const content = extractCurlyBracesContent(formattedConfigString);
|
||||
const cleanConfig = cleanContent(content);
|
||||
|
||||
// Format the config to ensure it's valid
|
||||
await prettier.format(`(${cleanConfig})`, {
|
||||
semi: false,
|
||||
parser: 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
|
||||
return `// @OVERRIDE:${cleanConfig}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the override pragma comment in source code.
|
||||
*/
|
||||
export function updateSourceWithOverridePragma(
|
||||
source: string,
|
||||
newPragma: string,
|
||||
): string {
|
||||
const firstLineEnd = source.indexOf('\n');
|
||||
const firstLine = source.substring(0, firstLineEnd);
|
||||
|
||||
const pragmaRegex = /^\/\/\s*@/;
|
||||
if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) {
|
||||
return newPragma + source.substring(firstLineEnd);
|
||||
} else {
|
||||
return newPragma + '\n' + source;
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,10 @@ export default function MyApp() {
|
||||
|
||||
export const defaultStore: Store = {
|
||||
source: index,
|
||||
config: '',
|
||||
};
|
||||
|
||||
export const emptyStore: Store = {
|
||||
source: '',
|
||||
config: '',
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import {defaultStore} from '../defaultStore';
|
||||
*/
|
||||
export interface Store {
|
||||
source: string;
|
||||
config?: string;
|
||||
}
|
||||
export function encodeStore(store: Store): string {
|
||||
return compressToEncodedURIComponent(JSON.stringify(store));
|
||||
@@ -65,5 +66,14 @@ export function initStoreFromUrlOrLocalStorage(): Store {
|
||||
const raw = decodeStore(encodedSource);
|
||||
|
||||
invariant(isValidStore(raw), 'Invalid Store');
|
||||
|
||||
// Add config property if missing for backwards compatibility
|
||||
if (!('config' in raw)) {
|
||||
return {
|
||||
...raw,
|
||||
config: '',
|
||||
};
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -34,26 +34,30 @@
|
||||
"invariant": "^2.2.4",
|
||||
"lz-string": "^1.5.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"next": "^15.2.0-canary.64",
|
||||
"next": "15.5.2",
|
||||
"notistack": "^3.0.0-alpha.7",
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-format": "^29.3.1",
|
||||
"re-resizable": "^6.9.16",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
"concurrently": "^7.4.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-next": "^15.0.1",
|
||||
"eslint-config-next": "15.5.2",
|
||||
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
+925
-249
File diff suppressed because it is too large
Load Diff
@@ -255,11 +255,16 @@ function parseConfigStringAsJS(
|
||||
|
||||
console.log('OVERRIDE:', parsedConfig);
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
'',
|
||||
defaults.environment ?? {},
|
||||
);
|
||||
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
compilationMode: defaults.compilationMode,
|
||||
environment: defaults.environment ?? defaultOptions.environment,
|
||||
environment,
|
||||
};
|
||||
|
||||
// Apply parsed config, merging environment if it exists
|
||||
@@ -269,22 +274,9 @@ function parseConfigStringAsJS(
|
||||
...parsedConfig.environment,
|
||||
};
|
||||
|
||||
// Apply complex defaults for environment flags that are set to true
|
||||
const environmentConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
|
||||
{};
|
||||
for (const [key, value] of Object.entries(mergedEnvironment)) {
|
||||
if (hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
|
||||
if (value === true && key in testComplexConfigDefaults) {
|
||||
environmentConfig[key] = testComplexConfigDefaults[key];
|
||||
} else {
|
||||
environmentConfig[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate environment config
|
||||
const validatedEnvironment =
|
||||
EnvironmentConfigSchema.safeParse(environmentConfig);
|
||||
EnvironmentConfigSchema.safeParse(mergedEnvironment);
|
||||
if (!validatedEnvironment.success) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Invalid environment configuration in config pragma',
|
||||
@@ -294,10 +286,6 @@ function parseConfigStringAsJS(
|
||||
});
|
||||
}
|
||||
|
||||
if (validatedEnvironment.data.enableResetCacheOnSourceFileChanges == null) {
|
||||
validatedEnvironment.data.enableResetCacheOnSourceFileChanges = false;
|
||||
}
|
||||
|
||||
options.environment = validatedEnvironment.data;
|
||||
}
|
||||
|
||||
@@ -308,9 +296,7 @@ function parseConfigStringAsJS(
|
||||
}
|
||||
|
||||
if (hasOwnProperty(defaultOptions, key)) {
|
||||
if (value === true && key in testComplexPluginOptionDefaults) {
|
||||
options[key] = testComplexPluginOptionDefaults[key];
|
||||
} else if (key === 'target' && value === 'donotuse_meta_internal') {
|
||||
if (key === 'target' && value === 'donotuse_meta_internal') {
|
||||
options[key] = {
|
||||
kind: value,
|
||||
runtimeModule: 'react',
|
||||
|
||||
@@ -48,10 +48,7 @@ export {
|
||||
printReactiveFunction,
|
||||
printReactiveFunctionWithOutlined,
|
||||
} from './ReactiveScopes';
|
||||
export {
|
||||
parseConfigPragmaForTests,
|
||||
parseConfigPragmaAsString,
|
||||
} from './Utils/TestUtils';
|
||||
export {parseConfigPragmaForTests} from './Utils/TestUtils';
|
||||
declare global {
|
||||
let __DEV__: boolean | null | undefined;
|
||||
}
|
||||
|
||||
@@ -74,13 +74,7 @@ function getDebugChannel(req) {
|
||||
return activeDebugChannels.get(requestId);
|
||||
}
|
||||
|
||||
async function renderApp(
|
||||
res,
|
||||
returnValue,
|
||||
formState,
|
||||
noCache,
|
||||
promiseForDebugChannel
|
||||
) {
|
||||
async function renderApp(res, returnValue, formState, noCache, debugChannel) {
|
||||
const {renderToPipeableStream} = await import(
|
||||
'react-server-dom-webpack/server'
|
||||
);
|
||||
@@ -132,7 +126,7 @@ async function renderApp(
|
||||
// For client-invoked server actions we refresh the tree and return a return value.
|
||||
const payload = {root, returnValue, formState};
|
||||
const {pipe} = renderToPipeableStream(payload, moduleMap, {
|
||||
debugChannel: await promiseForDebugChannel,
|
||||
debugChannel,
|
||||
filterStackFrame,
|
||||
});
|
||||
pipe(res);
|
||||
@@ -385,23 +379,20 @@ app.on('error', function (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Open a websocket server for Debug information
|
||||
const WebSocket = require('ws');
|
||||
const webSocketServer = new WebSocket.Server({noServer: true});
|
||||
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
const DEBUG_CHANNEL_PATH = '/debug-channel?';
|
||||
if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
|
||||
const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
|
||||
const promiseForWs = new Promise(resolve => {
|
||||
webSocketServer.handleUpgrade(request, socket, head, ws => {
|
||||
ws.on('close', () => {
|
||||
activeDebugChannels.delete(requestId);
|
||||
});
|
||||
resolve(ws);
|
||||
});
|
||||
});
|
||||
activeDebugChannels.set(requestId, promiseForWs);
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
server: httpServer,
|
||||
path: '/debug-channel',
|
||||
});
|
||||
|
||||
webSocketServer.on('connection', (ws, req) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const requestId = url.searchParams.get('id');
|
||||
|
||||
activeDebugChannels.set(requestId, ws);
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
activeDebugChannels.delete(requestId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,18 +14,52 @@ function findSourceMapURL(fileName) {
|
||||
);
|
||||
}
|
||||
|
||||
async function createWebSocketStream(url) {
|
||||
const ws = new WebSocket(url);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.addEventListener('open', resolve, {once: true});
|
||||
ws.addEventListener('error', reject, {once: true});
|
||||
});
|
||||
|
||||
const writable = new WritableStream({
|
||||
write(chunk) {
|
||||
ws.send(chunk);
|
||||
},
|
||||
close() {
|
||||
ws.close();
|
||||
},
|
||||
abort(reason) {
|
||||
ws.close(1000, reason && String(reason));
|
||||
},
|
||||
});
|
||||
|
||||
const readable = new ReadableStream({
|
||||
start(controller) {
|
||||
ws.addEventListener('message', event => {
|
||||
controller.enqueue(event.data);
|
||||
});
|
||||
ws.addEventListener('close', () => {
|
||||
controller.close();
|
||||
});
|
||||
ws.addEventListener('error', err => {
|
||||
controller.error(err);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {readable, writable};
|
||||
}
|
||||
|
||||
let updateRoot;
|
||||
async function callServer(id, args) {
|
||||
let response;
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
typeof WebSocketStream === 'function'
|
||||
) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const requestId = crypto.randomUUID();
|
||||
const wss = new WebSocketStream(
|
||||
'ws://localhost:3001/debug-channel?' + requestId
|
||||
const debugChannel = await createWebSocketStream(
|
||||
`ws://localhost:3001/debug-channel?id=${requestId}`
|
||||
);
|
||||
const debugChannel = await wss.opened;
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
method: 'POST',
|
||||
@@ -74,15 +108,11 @@ function Shell({data}) {
|
||||
|
||||
async function hydrateApp() {
|
||||
let response;
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
typeof WebSocketStream === 'function'
|
||||
) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const requestId = crypto.randomUUID();
|
||||
const wss = new WebSocketStream(
|
||||
'ws://localhost:3001/debug-channel?' + requestId
|
||||
const debugChannel = await createWebSocketStream(
|
||||
`ws://localhost:3001/debug-channel?id=${requestId}`
|
||||
);
|
||||
const debugChannel = await wss.opened;
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
|
||||
+40
-12
@@ -1010,10 +1010,15 @@ export function reportGlobalError(
|
||||
if (__DEV__) {
|
||||
const debugChannel = response._debugChannel;
|
||||
if (debugChannel !== undefined) {
|
||||
// If we don't have any more ways of reading data, we don't have to send any
|
||||
// more neither. So we close the writable side.
|
||||
// If we don't have any more ways of reading data, we don't have to send
|
||||
// any more neither. So we close the writable side.
|
||||
closeDebugChannel(debugChannel);
|
||||
response._debugChannel = undefined;
|
||||
// Make sure the debug channel is not closed a second time when the
|
||||
// Response gets GC:ed.
|
||||
if (debugChannelRegistry !== null) {
|
||||
debugChannelRegistry.unregister(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1069,7 +1074,14 @@ function getTaskName(type: mixed): string {
|
||||
}
|
||||
}
|
||||
|
||||
function initializeElement(response: Response, element: any): void {
|
||||
function initializeElement(
|
||||
response: Response,
|
||||
element: any,
|
||||
lazyType: null | LazyComponent<
|
||||
React$Element<any>,
|
||||
SomeChunk<React$Element<any>>,
|
||||
>,
|
||||
): void {
|
||||
if (!__DEV__) {
|
||||
return;
|
||||
}
|
||||
@@ -1136,6 +1148,18 @@ function initializeElement(response: Response, element: any): void {
|
||||
if (owner !== null) {
|
||||
initializeFakeStack(response, owner);
|
||||
}
|
||||
|
||||
// In case the JSX runtime has validated the lazy type as a static child, we
|
||||
// need to transfer this information to the element.
|
||||
if (
|
||||
lazyType &&
|
||||
lazyType._store &&
|
||||
lazyType._store.validated &&
|
||||
!element._store.validated
|
||||
) {
|
||||
element._store.validated = lazyType._store.validated;
|
||||
}
|
||||
|
||||
// TODO: We should be freezing the element but currently, we might write into
|
||||
// _debugInfo later. We could move it into _store which remains mutable.
|
||||
Object.freeze(element.props);
|
||||
@@ -1148,7 +1172,7 @@ function createElement(
|
||||
props: mixed,
|
||||
owner: ?ReactComponentInfo, // DEV-only
|
||||
stack: ?ReactStackTrace, // DEV-only
|
||||
validated: number, // DEV-only
|
||||
validated: 0 | 1 | 2, // DEV-only
|
||||
):
|
||||
| React$Element<any>
|
||||
| LazyComponent<React$Element<any>, SomeChunk<React$Element<any>>> {
|
||||
@@ -1225,7 +1249,7 @@ function createElement(
|
||||
handler.reason,
|
||||
);
|
||||
if (__DEV__) {
|
||||
initializeElement(response, element);
|
||||
initializeElement(response, element, null);
|
||||
// Conceptually the error happened inside this Element but right before
|
||||
// it was rendered. We don't have a client side component to render but
|
||||
// we can add some DebugInfo to explain that this was conceptually a
|
||||
@@ -1244,7 +1268,7 @@ function createElement(
|
||||
}
|
||||
erroredChunk._debugInfo = [erroredComponent];
|
||||
}
|
||||
return createLazyChunkWrapper(erroredChunk);
|
||||
return createLazyChunkWrapper(erroredChunk, validated);
|
||||
}
|
||||
if (handler.deps > 0) {
|
||||
// We have blocked references inside this Element but we can turn this into
|
||||
@@ -1253,16 +1277,17 @@ function createElement(
|
||||
createBlockedChunk(response);
|
||||
handler.value = element;
|
||||
handler.chunk = blockedChunk;
|
||||
const lazyType = createLazyChunkWrapper(blockedChunk, validated);
|
||||
if (__DEV__) {
|
||||
/// After we have initialized any blocked references, initialize stack etc.
|
||||
const init = initializeElement.bind(null, response, element);
|
||||
// After we have initialized any blocked references, initialize stack etc.
|
||||
const init = initializeElement.bind(null, response, element, lazyType);
|
||||
blockedChunk.then(init, init);
|
||||
}
|
||||
return createLazyChunkWrapper(blockedChunk);
|
||||
return lazyType;
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
initializeElement(response, element);
|
||||
initializeElement(response, element, null);
|
||||
}
|
||||
|
||||
return element;
|
||||
@@ -1270,6 +1295,7 @@ function createElement(
|
||||
|
||||
function createLazyChunkWrapper<T>(
|
||||
chunk: SomeChunk<T>,
|
||||
validated: 0 | 1 | 2, // DEV-only
|
||||
): LazyComponent<T, SomeChunk<T>> {
|
||||
const lazyType: LazyComponent<T, SomeChunk<T>> = {
|
||||
$$typeof: REACT_LAZY_TYPE,
|
||||
@@ -1281,6 +1307,8 @@ function createLazyChunkWrapper<T>(
|
||||
const chunkDebugInfo: ReactDebugInfo =
|
||||
chunk._debugInfo || (chunk._debugInfo = ([]: ReactDebugInfo));
|
||||
lazyType._debugInfo = chunkDebugInfo;
|
||||
// Initialize a store for key validation by the JSX runtime.
|
||||
lazyType._store = {validated: validated};
|
||||
}
|
||||
return lazyType;
|
||||
}
|
||||
@@ -2085,7 +2113,7 @@ function parseModelString(
|
||||
}
|
||||
// We create a React.lazy wrapper around any lazy values.
|
||||
// When passed into React, we'll know how to suspend on this.
|
||||
return createLazyChunkWrapper(chunk);
|
||||
return createLazyChunkWrapper(chunk, 0);
|
||||
}
|
||||
case '@': {
|
||||
// Promise
|
||||
@@ -2434,7 +2462,7 @@ function ResponseInstance(
|
||||
// When a Response gets GC:ed because nobody is referring to any of the
|
||||
// objects that lazily load from the Response anymore, then we can close
|
||||
// the debug channel.
|
||||
debugChannelRegistry.register(this, debugChannel);
|
||||
debugChannelRegistry.register(this, debugChannel, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
};
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
const handleClick = ({metaKey}) => {
|
||||
if (id !== null) {
|
||||
const handleClick = ({metaKey, button}) => {
|
||||
if (id !== null && button === 0) {
|
||||
logEvent({
|
||||
event_name: 'select-element',
|
||||
metadata: {source: 'click-element'},
|
||||
|
||||
@@ -16,14 +16,15 @@
|
||||
|
||||
.TreeWrapper {
|
||||
border-top: 1px solid var(--color-border);
|
||||
flex: 1 1 var(--horizontal-resize-tree-percentage);
|
||||
flex: 1 1 65%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.InspectedElementWrapper {
|
||||
flex: 1 1 35%;
|
||||
flex: 0 0 calc(100% - var(--horizontal-resize-tree-percentage));
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -59,12 +60,12 @@
|
||||
|
||||
.TreeWrapper {
|
||||
border-top: 1px solid var(--color-border);
|
||||
flex: 1 1 var(--vertical-resize-tree-percentage);
|
||||
flex: 1 1 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.InspectedElementWrapper {
|
||||
flex: 1 1 50%;
|
||||
flex: 0 0 calc(100% - var(--vertical-resize-tree-percentage));
|
||||
}
|
||||
|
||||
.TreeWrapper + .ResizeBarWrapper .ResizeBar {
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 0.25rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.SuspenseTimelineInput {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
/*
|
||||
* `overflow: auto` will add scrollbars but the input will not actually grow beyond visible content.
|
||||
* `overflow: hidden` will constrain the input to its visible content.
|
||||
*/
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.SuspenseTimelineRootSwitcher {
|
||||
@@ -16,20 +21,6 @@
|
||||
max-width: 3rem;
|
||||
}
|
||||
|
||||
.SuspenseTimelineMarkers {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
.SuspenseTimelineProgressIndicator {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.SuspenseTimelineMarkers > * {
|
||||
flex: 1 1 0;
|
||||
overflow: visible;
|
||||
visibility: hidden;
|
||||
width: 0
|
||||
}
|
||||
|
||||
.SuspenseTimelineActiveMarker {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
||||
+21
-51
@@ -11,14 +11,7 @@ import type {Element, SuspenseNode} from '../../../frontend/types';
|
||||
import type Store from '../../store';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useContext,
|
||||
useId,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {useContext, useLayoutEffect, useMemo, useRef, useState} from 'react';
|
||||
import {BridgeContext, StoreContext} from '../context';
|
||||
import {TreeDispatcherContext} from '../Components/TreeContext';
|
||||
import {useHighlightHostInstance} from '../hooks';
|
||||
@@ -112,30 +105,6 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
|
||||
setValue(max);
|
||||
}
|
||||
|
||||
const markersID = useId();
|
||||
const markers: React.Node[] = useMemo(() => {
|
||||
return timeline.map((suspense, index) => {
|
||||
const takesUpSpace =
|
||||
suspense.rects !== null &&
|
||||
suspense.rects.some(rect => {
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
});
|
||||
|
||||
return takesUpSpace ? (
|
||||
<option
|
||||
key={suspense.id}
|
||||
className={
|
||||
index === value ? styles.SuspenseTimelineActiveMarker : undefined
|
||||
}
|
||||
value={index}>
|
||||
#{index + 1}
|
||||
</option>
|
||||
) : (
|
||||
<option key={suspense.id} />
|
||||
);
|
||||
});
|
||||
}, [timeline, value]);
|
||||
|
||||
if (rootID === undefined) {
|
||||
return <div className={styles.SuspenseTimelineInput}>Root not found.</div>;
|
||||
}
|
||||
@@ -219,25 +188,26 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.SuspenseTimelineInput}>
|
||||
<input
|
||||
className={styles.SuspenseTimelineSlider}
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
list={markersID}
|
||||
value={value}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={clearHighlightHostInstance}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<datalist id={markersID} className={styles.SuspenseTimelineMarkers}>
|
||||
{markers}
|
||||
</datalist>
|
||||
</div>
|
||||
<>
|
||||
<div>
|
||||
{value}/{max}
|
||||
</div>
|
||||
<div className={styles.SuspenseTimelineInput}>
|
||||
<input
|
||||
className={styles.SuspenseTimelineSlider}
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={clearHighlightHostInstance}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -10,5 +10,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default function SuspenseTreeList(_: {}): React$Node {
|
||||
return <div>Activity slices</div>;
|
||||
return <div>Activity slices not implemented yet</div>;
|
||||
}
|
||||
|
||||
+3
-1
@@ -126,6 +126,7 @@ function wwwOnCaughtError(
|
||||
|
||||
defaultOnCaughtError(error, errorInfo);
|
||||
}
|
||||
const noopOnDefaultTransitionIndicator = noop;
|
||||
|
||||
export function createRoot(
|
||||
container: Element | Document | DocumentFragment,
|
||||
@@ -137,6 +138,7 @@ export function createRoot(
|
||||
({
|
||||
onUncaughtError: wwwOnUncaughtError,
|
||||
onCaughtError: wwwOnCaughtError,
|
||||
onDefaultTransitionIndicator: noopOnDefaultTransitionIndicator,
|
||||
}: any),
|
||||
options,
|
||||
),
|
||||
@@ -155,6 +157,7 @@ export function hydrateRoot(
|
||||
({
|
||||
onUncaughtError: wwwOnUncaughtError,
|
||||
onCaughtError: wwwOnCaughtError,
|
||||
onDefaultTransitionIndicator: noopOnDefaultTransitionIndicator,
|
||||
}: any),
|
||||
options,
|
||||
),
|
||||
@@ -211,7 +214,6 @@ function getReactRootElementInContainer(container: any) {
|
||||
// This isn't reachable because onRecoverableError isn't called in the
|
||||
// legacy API.
|
||||
const noopOnRecoverableError = noop;
|
||||
const noopOnDefaultTransitionIndicator = noop;
|
||||
|
||||
function legacyCreateRootFromDOMContainer(
|
||||
container: Container,
|
||||
|
||||
+11
-2
@@ -46,7 +46,10 @@ import {
|
||||
createPublicRootInstance,
|
||||
type PublicRootInstance,
|
||||
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
||||
import {disableLegacyMode} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
disableLegacyMode,
|
||||
enableDefaultTransitionIndicator,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') {
|
||||
throw new Error(
|
||||
@@ -132,6 +135,12 @@ function render(
|
||||
if (options && options.onRecoverableError !== undefined) {
|
||||
onRecoverableError = options.onRecoverableError;
|
||||
}
|
||||
let onDefaultTransitionIndicator = nativeOnDefaultTransitionIndicator;
|
||||
if (enableDefaultTransitionIndicator) {
|
||||
if (options && options.onDefaultTransitionIndicator !== undefined) {
|
||||
onDefaultTransitionIndicator = options.onDefaultTransitionIndicator;
|
||||
}
|
||||
}
|
||||
|
||||
const publicRootInstance = createPublicRootInstance(containerTag);
|
||||
const rootInstance = {
|
||||
@@ -151,7 +160,7 @@ function render(
|
||||
onUncaughtError,
|
||||
onCaughtError,
|
||||
onRecoverableError,
|
||||
nativeOnDefaultTransitionIndicator,
|
||||
onDefaultTransitionIndicator,
|
||||
null,
|
||||
);
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export type RenderRootOptions = {
|
||||
error: mixed,
|
||||
errorInfo: {+componentStack?: ?string},
|
||||
) => void,
|
||||
onDefaultTransitionIndicator?: () => void | (() => void),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+17
-3
@@ -73,6 +73,7 @@ import {
|
||||
includesSomeLane,
|
||||
isGestureRender,
|
||||
GestureLane,
|
||||
UpdateLanes,
|
||||
} from './ReactFiberLane';
|
||||
import {
|
||||
ContinuousEventPriority,
|
||||
@@ -2983,6 +2984,20 @@ function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
|
||||
}
|
||||
}
|
||||
|
||||
function isRenderingDeferredWork(): boolean {
|
||||
if (!includesSomeLane(renderLanes, DeferredLane)) {
|
||||
// None of the render lanes are deferred lanes.
|
||||
return false;
|
||||
}
|
||||
// At least one of the render lanes are deferred lanes. However, if the
|
||||
// current render is also batched together with an update, then we can't
|
||||
// say that the render is wholly the result of deferred work. We can check
|
||||
// this by checking if the root render lanes contain any "update" lanes, i.e.
|
||||
// lanes that are only assigned to updates, like setState.
|
||||
const rootRenderLanes = getWorkInProgressRootRenderLanes();
|
||||
return !includesSomeLane(rootRenderLanes, UpdateLanes);
|
||||
}
|
||||
|
||||
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
|
||||
if (
|
||||
// When `initialValue` is provided, we defer the initial render even if the
|
||||
@@ -2991,7 +3006,7 @@ function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
|
||||
// However, to avoid waterfalls, we do not defer if this render
|
||||
// was itself spawned by an earlier useDeferredValue. Check if DeferredLane
|
||||
// is part of the render lanes.
|
||||
!includesSomeLane(renderLanes, DeferredLane)
|
||||
!isRenderingDeferredWork()
|
||||
) {
|
||||
// Render with the initial value
|
||||
hook.memoizedState = initialValue;
|
||||
@@ -3038,8 +3053,7 @@ function updateDeferredValueImpl<T>(
|
||||
}
|
||||
|
||||
const shouldDeferValue =
|
||||
!includesOnlyNonUrgentLanes(renderLanes) &&
|
||||
!includesSomeLane(renderLanes, DeferredLane);
|
||||
!includesOnlyNonUrgentLanes(renderLanes) && !isRenderingDeferredWork();
|
||||
if (shouldDeferValue) {
|
||||
// This is an urgent update. Since the value has changed, keep using the
|
||||
// previous value and spawn a deferred render to update it later.
|
||||
|
||||
+41
-8
@@ -73,6 +73,20 @@ const TransitionLane12: Lane = /* */ 0b0000000000010000000
|
||||
const TransitionLane13: Lane = /* */ 0b0000000000100000000000000000000;
|
||||
const TransitionLane14: Lane = /* */ 0b0000000001000000000000000000000;
|
||||
|
||||
const TransitionUpdateLanes =
|
||||
TransitionLane1 |
|
||||
TransitionLane2 |
|
||||
TransitionLane3 |
|
||||
TransitionLane4 |
|
||||
TransitionLane5 |
|
||||
TransitionLane6 |
|
||||
TransitionLane7 |
|
||||
TransitionLane8 |
|
||||
TransitionLane9 |
|
||||
TransitionLane10;
|
||||
const TransitionDeferredLanes =
|
||||
TransitionLane11 | TransitionLane12 | TransitionLane13 | TransitionLane14;
|
||||
|
||||
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
|
||||
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
|
||||
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
|
||||
@@ -94,7 +108,7 @@ export const DeferredLane: Lane = /* */ 0b1000000000000000000
|
||||
// Any lane that might schedule an update. This is used to detect infinite
|
||||
// update loops, so it doesn't include hydration lanes or retries.
|
||||
export const UpdateLanes: Lanes =
|
||||
SyncLane | InputContinuousLane | DefaultLane | TransitionLanes;
|
||||
SyncLane | InputContinuousLane | DefaultLane | TransitionUpdateLanes;
|
||||
|
||||
export const HydrationLanes =
|
||||
SyncHydrationLane |
|
||||
@@ -155,7 +169,8 @@ export function getLabelForLane(lane: Lane): string | void {
|
||||
|
||||
export const NoTimestamp = -1;
|
||||
|
||||
let nextTransitionLane: Lane = TransitionLane1;
|
||||
let nextTransitionUpdateLane: Lane = TransitionLane1;
|
||||
let nextTransitionDeferredLane: Lane = TransitionLane11;
|
||||
let nextRetryLane: Lane = RetryLane1;
|
||||
|
||||
function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
|
||||
@@ -190,11 +205,12 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
|
||||
case TransitionLane8:
|
||||
case TransitionLane9:
|
||||
case TransitionLane10:
|
||||
return lanes & TransitionUpdateLanes;
|
||||
case TransitionLane11:
|
||||
case TransitionLane12:
|
||||
case TransitionLane13:
|
||||
case TransitionLane14:
|
||||
return lanes & TransitionLanes;
|
||||
return lanes & TransitionDeferredLanes;
|
||||
case RetryLane1:
|
||||
case RetryLane2:
|
||||
case RetryLane3:
|
||||
@@ -679,14 +695,23 @@ export function isGestureRender(lanes: Lanes): boolean {
|
||||
return lanes === GestureLane;
|
||||
}
|
||||
|
||||
export function claimNextTransitionLane(): Lane {
|
||||
export function claimNextTransitionUpdateLane(): Lane {
|
||||
// Cycle through the lanes, assigning each new transition to the next lane.
|
||||
// In most cases, this means every transition gets its own lane, until we
|
||||
// run out of lanes and cycle back to the beginning.
|
||||
const lane = nextTransitionLane;
|
||||
nextTransitionLane <<= 1;
|
||||
if ((nextTransitionLane & TransitionLanes) === NoLanes) {
|
||||
nextTransitionLane = TransitionLane1;
|
||||
const lane = nextTransitionUpdateLane;
|
||||
nextTransitionUpdateLane <<= 1;
|
||||
if ((nextTransitionUpdateLane & TransitionUpdateLanes) === NoLanes) {
|
||||
nextTransitionUpdateLane = TransitionLane1;
|
||||
}
|
||||
return lane;
|
||||
}
|
||||
|
||||
export function claimNextTransitionDeferredLane(): Lane {
|
||||
const lane = nextTransitionDeferredLane;
|
||||
nextTransitionDeferredLane <<= 1;
|
||||
if ((nextTransitionDeferredLane & TransitionDeferredLanes) === NoLanes) {
|
||||
nextTransitionDeferredLane = TransitionLane11;
|
||||
}
|
||||
return lane;
|
||||
}
|
||||
@@ -952,6 +977,14 @@ function markSpawnedDeferredLane(
|
||||
// Entangle the spawned lane with the DeferredLane bit so that we know it
|
||||
// was the result of another render. This lets us avoid a useDeferredValue
|
||||
// waterfall — only the first level will defer.
|
||||
// TODO: Now that there is a reserved set of transition lanes that are used
|
||||
// exclusively for deferred work, we should get rid of this special
|
||||
// DeferredLane bit; the same information can be inferred by checking whether
|
||||
// the lane is one of the TransitionDeferredLanes. The only reason this still
|
||||
// exists is because we need to also do the same for OffscreenLane. That
|
||||
// requires additional changes because there are more places around the
|
||||
// codebase that treat OffscreenLane as a magic value; would need to check
|
||||
// for a new OffscreenDeferredLane, too. Will leave this for a follow-up.
|
||||
const spawnedLaneIndex = laneToIndex(spawnedLane);
|
||||
root.entangledLanes |= spawnedLane;
|
||||
root.entanglements[spawnedLaneIndex] |=
|
||||
|
||||
+46
-28
@@ -228,9 +228,20 @@ export function logComponentRender(
|
||||
? 'tertiary-dark'
|
||||
: 'primary-dark'
|
||||
: 'error';
|
||||
const debugTask = fiber._debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
|
||||
if (!__DEV__) {
|
||||
console.timeStamp(
|
||||
name,
|
||||
startTime,
|
||||
endTime,
|
||||
COMPONENTS_TRACK,
|
||||
undefined,
|
||||
color,
|
||||
);
|
||||
} else {
|
||||
const props = fiber.memoizedProps;
|
||||
const debugTask = fiber._debugTask;
|
||||
|
||||
if (
|
||||
props !== null &&
|
||||
alternate !== null &&
|
||||
@@ -268,38 +279,45 @@ export function logComponentRender(
|
||||
reusableComponentDevToolDetails.properties = properties;
|
||||
reusableComponentOptions.start = startTime;
|
||||
reusableComponentOptions.end = endTime;
|
||||
|
||||
if (debugTask != null) {
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(
|
||||
performance,
|
||||
'\u200b' + name,
|
||||
reusableComponentOptions,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
performance.measure('\u200b' + name, reusableComponentOptions);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (debugTask != null) {
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(
|
||||
performance,
|
||||
'\u200b' + name,
|
||||
reusableComponentOptions,
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
name,
|
||||
startTime,
|
||||
endTime,
|
||||
COMPONENTS_TRACK,
|
||||
undefined,
|
||||
color,
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
console.timeStamp(
|
||||
name,
|
||||
startTime,
|
||||
endTime,
|
||||
COMPONENTS_TRACK,
|
||||
undefined,
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
name,
|
||||
startTime,
|
||||
endTime,
|
||||
COMPONENTS_TRACK,
|
||||
undefined,
|
||||
color,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
name,
|
||||
startTime,
|
||||
endTime,
|
||||
COMPONENTS_TRACK,
|
||||
undefined,
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
getNextLanes,
|
||||
includesSyncLane,
|
||||
markStarvedLanesAsExpired,
|
||||
claimNextTransitionLane,
|
||||
claimNextTransitionUpdateLane,
|
||||
getNextLanesToFlushSync,
|
||||
checkIfRootIsPrerendering,
|
||||
isGestureRender,
|
||||
@@ -716,7 +716,7 @@ export function requestTransitionLane(
|
||||
: // We may or may not be inside an async action scope. If we are, this
|
||||
// is the first update in that scope. Either way, we need to get a
|
||||
// fresh transition lane.
|
||||
claimNextTransitionLane();
|
||||
claimNextTransitionUpdateLane();
|
||||
}
|
||||
return currentEventTransitionLane;
|
||||
}
|
||||
|
||||
+2
-2
@@ -192,7 +192,7 @@ import {
|
||||
OffscreenLane,
|
||||
SyncUpdateLanes,
|
||||
UpdateLanes,
|
||||
claimNextTransitionLane,
|
||||
claimNextTransitionDeferredLane,
|
||||
checkIfRootIsPrerendering,
|
||||
includesOnlyViewTransitionEligibleLanes,
|
||||
isGestureRender,
|
||||
@@ -827,7 +827,7 @@ export function requestDeferredLane(): Lane {
|
||||
workInProgressDeferredLane = OffscreenLane;
|
||||
} else {
|
||||
// Everything else is spawned as a transition.
|
||||
workInProgressDeferredLane = claimNextTransitionLane();
|
||||
workInProgressDeferredLane = claimNextTransitionDeferredLane();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -608,6 +608,48 @@ describe('ReactDeferredValue', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"regression: useDeferredValue's initial value argument works even if an unrelated " +
|
||||
'transition is suspended',
|
||||
async () => {
|
||||
// Simulates a previous bug where a new useDeferredValue hook is mounted
|
||||
// while some unrelated transition is suspended. In the regression case,
|
||||
// the initial values was skipped/ignored.
|
||||
|
||||
function Content({text}) {
|
||||
return (
|
||||
<AsyncText text={useDeferredValue(text, `Preview ${text}...`)} />
|
||||
);
|
||||
}
|
||||
|
||||
function App({text}) {
|
||||
// Use a key to force a new Content instance to be mounted each time
|
||||
// the text changes.
|
||||
return <Content key={text} text={text} />;
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
// Render a previous UI using useDeferredValue. Suspend on the
|
||||
// final value.
|
||||
resolveText('Preview A...');
|
||||
await act(() => startTransition(() => root.render(<App text="A" />)));
|
||||
assertLog(['Preview A...', 'Suspend! [A]']);
|
||||
|
||||
// While it's still suspended, update the UI to show a different screen
|
||||
// with a different preview value. We should be able to show the new
|
||||
// preview even though the previous transition never finished.
|
||||
resolveText('Preview B...');
|
||||
await act(() => startTransition(() => root.render(<App text="B" />)));
|
||||
assertLog(['Preview B...', 'Suspend! [B]']);
|
||||
|
||||
// Now finish loading the final value.
|
||||
await act(() => resolveText('B'));
|
||||
assertLog(['B']);
|
||||
expect(root).toMatchRenderedOutput('B');
|
||||
},
|
||||
);
|
||||
|
||||
it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => {
|
||||
// Same as the previous test but with a Suspense boundary separating the
|
||||
// two useDeferredValue hooks.
|
||||
|
||||
+60
@@ -2846,4 +2846,64 @@ describe('ReactFlightDOMBrowser', () => {
|
||||
|
||||
expect(container.innerHTML).toBe('<p>Hi</p>');
|
||||
});
|
||||
|
||||
it('should not have missing key warnings when a static child is blocked on debug info', async () => {
|
||||
const ClientComponent = clientExports(function ClientComponent({element}) {
|
||||
return (
|
||||
<div>
|
||||
<span>Hi</span>
|
||||
{element}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
let debugReadableStreamController;
|
||||
|
||||
const debugReadableStream = new ReadableStream({
|
||||
start(controller) {
|
||||
debugReadableStreamController = controller;
|
||||
},
|
||||
});
|
||||
|
||||
const stream = await serverAct(() =>
|
||||
ReactServerDOMServer.renderToReadableStream(
|
||||
<ClientComponent element={<span>Sebbie</span>} />,
|
||||
webpackMap,
|
||||
{
|
||||
debugChannel: {
|
||||
writable: new WritableStream({
|
||||
write(chunk) {
|
||||
debugReadableStreamController.enqueue(chunk);
|
||||
},
|
||||
close() {
|
||||
debugReadableStreamController.close();
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
function ClientRoot({response}) {
|
||||
return use(response);
|
||||
}
|
||||
|
||||
const response = ReactServerDOMClient.createFromReadableStream(stream, {
|
||||
debugChannel: {readable: createDelayedStream(debugReadableStream)},
|
||||
});
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
await act(() => {
|
||||
root.render(<ClientRoot response={response} />);
|
||||
});
|
||||
|
||||
// Wait for the debug info to be processed.
|
||||
await act(() => {});
|
||||
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><span>Hi</span><span>Sebbie</span></div>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,10 @@ export type LazyComponent<T, P> = {
|
||||
$$typeof: symbol | number,
|
||||
_payload: P,
|
||||
_init: (payload: P) => T,
|
||||
|
||||
// __DEV__
|
||||
_debugInfo?: null | ReactDebugInfo,
|
||||
_store?: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail
|
||||
};
|
||||
|
||||
function lazyInitializer<T>(payload: Payload<T>): T {
|
||||
|
||||
@@ -804,6 +804,14 @@ function validateChildKeys(node) {
|
||||
if (node._store) {
|
||||
node._store.validated = 1;
|
||||
}
|
||||
} else if (isLazyType(node)) {
|
||||
if (node._payload.status === 'fulfilled') {
|
||||
if (isValidElement(node._payload.value) && node._payload.value._store) {
|
||||
node._payload.value._store.validated = 1;
|
||||
}
|
||||
} else if (node._store) {
|
||||
node._store.validated = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -822,3 +830,11 @@ export function isValidElement(object) {
|
||||
object.$$typeof === REACT_ELEMENT_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
export function isLazyType(object) {
|
||||
return (
|
||||
typeof object === 'object' &&
|
||||
object !== null &&
|
||||
object.$$typeof === REACT_LAZY_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export const enableSuspenseyImages: boolean = false;
|
||||
export const enableFizzBlockingRender: boolean = true;
|
||||
export const enableSrcObject: boolean = false;
|
||||
export const enableHydrationChangeEvent: boolean = true;
|
||||
export const enableDefaultTransitionIndicator: boolean = false;
|
||||
export const enableDefaultTransitionIndicator: boolean = true;
|
||||
export const ownerStackLimit = 1e4;
|
||||
export const enableComponentPerformanceTrack: boolean =
|
||||
__PROFILE__ && dynamicFlags.enableComponentPerformanceTrack;
|
||||
|
||||
@@ -66,7 +66,7 @@ export const enableSuspenseyImages = false;
|
||||
export const enableFizzBlockingRender = true;
|
||||
export const enableSrcObject = false;
|
||||
export const enableHydrationChangeEvent = false;
|
||||
export const enableDefaultTransitionIndicator = false;
|
||||
export const enableDefaultTransitionIndicator = true;
|
||||
export const enableFragmentRefs = false;
|
||||
export const enableFragmentRefsScrollIntoView = false;
|
||||
export const ownerStackLimit = 1e4;
|
||||
|
||||
@@ -79,7 +79,7 @@ export const enableSuspenseyImages: boolean = false;
|
||||
export const enableFizzBlockingRender: boolean = true;
|
||||
export const enableSrcObject: boolean = false;
|
||||
export const enableHydrationChangeEvent: boolean = false;
|
||||
export const enableDefaultTransitionIndicator: boolean = false;
|
||||
export const enableDefaultTransitionIndicator: boolean = true;
|
||||
|
||||
export const enableFragmentRefs: boolean = false;
|
||||
export const enableFragmentRefsScrollIntoView: boolean = false;
|
||||
|
||||
@@ -109,7 +109,7 @@ export const enableSuspenseyImages: boolean = false;
|
||||
export const enableFizzBlockingRender: boolean = true;
|
||||
export const enableSrcObject: boolean = false;
|
||||
export const enableHydrationChangeEvent: boolean = false;
|
||||
export const enableDefaultTransitionIndicator: boolean = false;
|
||||
export const enableDefaultTransitionIndicator: boolean = true;
|
||||
|
||||
export const ownerStackLimit = 1e4;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user