Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa7193720b | ||
|
|
6eda534718 | ||
|
|
c03a51d836 | ||
|
|
ad578aa01f | ||
|
|
03a96c75db | ||
|
|
755cebad6b | ||
|
|
581321160f | ||
|
|
1bcdd224b1 | ||
|
|
84af9085c1 | ||
|
|
128abcfa01 | ||
|
|
e3c9656d20 | ||
|
|
27b4076ab0 | ||
|
|
81d66927af | ||
|
|
6a4c8f51fa | ||
|
|
16df13b84c | ||
|
|
7899729130 | ||
|
|
a51f925217 | ||
|
|
941cd803a7 | ||
|
|
851bad0c88 | ||
|
|
5e0c951b58 | ||
|
|
348a4e2d44 | ||
|
|
5d49b2b7f4 | ||
|
|
ae22247dce | ||
|
|
e3f191803c | ||
|
|
e12b0bdc3b | ||
|
|
92d7ad5dd9 | ||
|
|
67a44bcd1b | ||
|
|
3fa927b674 | ||
|
|
47664deb8e | ||
|
|
5502d85cc7 | ||
|
|
8a8e9a7edf | ||
|
|
68f00c901c | ||
|
|
93d7aa69b2 | ||
|
|
20e5431747 | ||
|
|
1a27af3607 | ||
|
|
0e10ee906e | ||
|
|
0c813c528d | ||
|
|
a9ad64c852 | ||
|
|
7fc888dde2 | ||
|
|
67415c8c4a | ||
|
|
f3a803617e | ||
|
|
fe84397e81 | ||
|
|
b1c519f3d4 | ||
|
|
8c1501452c | ||
|
|
bd9e6e0bed | ||
|
|
835b00908b | ||
|
|
e2ba45bb39 | ||
|
|
886b3d36d7 | ||
|
|
288d428af1 | ||
|
|
a34c5dff15 | ||
|
|
3bf8ab430e | ||
|
|
acada3035f |
49
.github/workflows/devtools_discord_notify.yml
vendored
Normal file
49
.github/workflows/devtools_discord_notify.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: (DevTools) Discord Notify
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
paths:
|
||||
- packages/react-devtools**
|
||||
- .github/workflows/devtools_**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check_access:
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
check_maintainer:
|
||||
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
|
||||
needs: [check_access]
|
||||
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
|
||||
permissions:
|
||||
# Used by check_maintainer
|
||||
contents: read
|
||||
with:
|
||||
actor: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
notify:
|
||||
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
|
||||
needs: check_maintainer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DEVTOOLS_DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: ${{ github.event.pull_request.user.login }}
|
||||
embed-author-url: ${{ github.event.pull_request.user.html_url }}
|
||||
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
|
||||
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
|
||||
embed-description: ${{ github.event.pull_request.body }}
|
||||
embed-url: ${{ github.event.pull_request.html_url }}
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools
|
||||
path: build/devtools.tgz
|
||||
path: build/devtools
|
||||
if-no-files-found: error
|
||||
# Simplifies getting the extension for local testing
|
||||
- name: Archive chrome extension
|
||||
@@ -201,5 +201,5 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: screenshots
|
||||
path: ./tmp/screenshots
|
||||
path: ./tmp/playwright-artifacts
|
||||
if-no-files-found: warn
|
||||
|
||||
13
.github/workflows/runtime_build_and_test.yml
vendored
13
.github/workflows/runtime_build_and_test.yml
vendored
@@ -766,6 +766,11 @@ jobs:
|
||||
name: react-devtools-${{ matrix.browser }}-extension
|
||||
path: build/devtools/${{ matrix.browser }}-extension.zip
|
||||
if-no-files-found: error
|
||||
- name: Archive ${{ matrix.browser }} metadata
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools-${{ matrix.browser }}-metadata
|
||||
path: build/devtools/webpack-stats.*.json
|
||||
|
||||
merge_devtools_artifacts:
|
||||
name: Merge DevTools artifacts
|
||||
@@ -776,7 +781,7 @@ jobs:
|
||||
uses: actions/upload-artifact/merge@v4
|
||||
with:
|
||||
name: react-devtools
|
||||
pattern: react-devtools-*-extension
|
||||
pattern: react-devtools-*
|
||||
|
||||
run_devtools_e2e_tests:
|
||||
name: Run DevTools e2e tests
|
||||
@@ -826,6 +831,12 @@ jobs:
|
||||
- run: ./scripts/ci/run_devtools_e2e_tests.js
|
||||
env:
|
||||
RELEASE_CHANNEL: experimental
|
||||
- name: Archive Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: devtools-playwright-artifacts
|
||||
path: tmp/playwright-artifacts
|
||||
if-no-files-found: warn
|
||||
|
||||
# ----- SIZEBOT -----
|
||||
sizebot:
|
||||
|
||||
2
.github/workflows/runtime_discord_notify.yml
vendored
2
.github/workflows/runtime_discord_notify.yml
vendored
@@ -4,8 +4,10 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
paths-ignore:
|
||||
- packages/react-devtools**
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_**.yml
|
||||
- .github/workflows/devtools**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ chrome-user-data
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
/tmp
|
||||
|
||||
packages/react-devtools-core/dist
|
||||
packages/react-devtools-extensions/chrome/build
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode:"all"
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -136,7 +136,7 @@ test('editor should compile from hash successfully', async ({page}) => {
|
||||
path: 'test-results/01-compiles-from-hash.png',
|
||||
});
|
||||
const text =
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? [];
|
||||
const output = await formatPrint(text);
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
@@ -162,7 +162,7 @@ test('reset button works', async ({page}) => {
|
||||
path: 'test-results/02-reset-button-works.png',
|
||||
});
|
||||
const text =
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? [];
|
||||
const output = await formatPrint(text);
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
@@ -183,7 +183,7 @@ TEST_CASE_INPUTS.forEach((t, idx) =>
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? [];
|
||||
let output: string;
|
||||
if (t.noFormat) {
|
||||
output = text.join('');
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type {NextPage} from 'next';
|
||||
import Head from 'next/head';
|
||||
import {SnackbarProvider} from 'notistack';
|
||||
import {Editor, Header, StoreProvider} from '../components';
|
||||
import MessageSnackbar from '../components/Message';
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<div className="flex flex-col w-screen h-screen font-light">
|
||||
<Head>
|
||||
<title>
|
||||
{process.env.NODE_ENV === 'development'
|
||||
? '[DEV] React Compiler Playground'
|
||||
: 'React Compiler Playground'}
|
||||
</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"></meta>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Source-Code-Pro-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Display_W_Lt.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</Head>
|
||||
<StoreProvider>
|
||||
<SnackbarProvider
|
||||
preventDuplicate
|
||||
maxSnack={10}
|
||||
Components={{message: MessageSnackbar}}>
|
||||
<Header />
|
||||
<Editor />
|
||||
</SnackbarProvider>
|
||||
</StoreProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
97
compiler/apps/playground/components/AccordionWindow.tsx
Normal file
97
compiler/apps/playground/components/AccordionWindow.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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 {Resizable} from 're-resizable';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
type TabsRecord = Map<string, React.ReactNode>;
|
||||
|
||||
export default function AccordionWindow(props: {
|
||||
defaultTab: string | null;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className="flex flex-row h-full">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<AccordionWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionWindowItem({
|
||||
name,
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
}): React.ReactElement {
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const toggleTabs = useCallback(() => {
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
}, [tabsOpen, name, setTabsOpen]);
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,86 +6,78 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {PluginOptions} from 'babel-plugin-react-compiler';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import React, {useState, useCallback} from 'react';
|
||||
import React, {useState, useRef, useEffect} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {
|
||||
ConfigError,
|
||||
generateOverridePragmaFromConfig,
|
||||
updateSourceWithOverridePragma,
|
||||
} from '../../lib/configUtils';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import prettyFormat from 'pretty-format';
|
||||
|
||||
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
|
||||
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor(): React.ReactElement {
|
||||
export default function ConfigEditor({
|
||||
appliedOptions,
|
||||
}: {
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): React.ReactElement {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
// TODO: Use <Activity> when it is compatible with Monaco: https://github.com/suren-atoyan/monaco-react/issues/753
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<ExpandedEditor
|
||||
onToggle={setIsExpanded}
|
||||
appliedOptions={appliedOptions}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: !isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<CollapsedEditor onToggle={setIsExpanded} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedEditor({
|
||||
onToggle,
|
||||
appliedOptions,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): React.ReactElement {
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setIsExpanded(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const handleApplyConfig: () => Promise<void> = async () => {
|
||||
try {
|
||||
const config = store.config || '';
|
||||
|
||||
if (!config.trim()) {
|
||||
enqueueSnackbar(
|
||||
'Config is empty. Please add configuration options first.',
|
||||
{
|
||||
variant: 'warning',
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
const newPragma = await generateOverridePragmaFromConfig(config);
|
||||
const updatedSource = updateSourceWithOverridePragma(
|
||||
store.source,
|
||||
newPragma,
|
||||
);
|
||||
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: updatedSource,
|
||||
config: config,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to apply config:', error);
|
||||
|
||||
if (error instanceof ConfigError && error.message.trim()) {
|
||||
enqueueSnackbar(error.message, {
|
||||
variant: 'error',
|
||||
});
|
||||
} else {
|
||||
enqueueSnackbar('Unexpected error: failed to apply config.', {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange: (value: string | undefined) => void = value => {
|
||||
const handleChange: (value: string | undefined) => void = (
|
||||
value: string | undefined,
|
||||
) => {
|
||||
if (value === undefined) return;
|
||||
|
||||
// Only update the config
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: store.source,
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
dispatchStore({
|
||||
type: 'updateConfig',
|
||||
payload: {
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
}, 500); // 500ms debounce delay
|
||||
};
|
||||
|
||||
const handleMount: (
|
||||
@@ -109,75 +101,116 @@ export default function ConfigEditor(): React.ReactElement {
|
||||
allowSyntheticDefaultImports: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
});
|
||||
|
||||
const uri = monaco.Uri.parse(`file:///config.ts`);
|
||||
const model = monaco.editor.getModel(uri);
|
||||
if (model) {
|
||||
model.updateOptions({tabSize: 2});
|
||||
}
|
||||
};
|
||||
|
||||
const formattedAppliedOptions = appliedOptions
|
||||
? prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
})
|
||||
: 'Invalid configs';
|
||||
|
||||
return (
|
||||
<div className="flex flex-row relative">
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<Resizable
|
||||
className="border-r"
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350, height: 'auto'}}
|
||||
enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize config editor"
|
||||
aria-label="Minimize config editor"
|
||||
onClick={toggleExpanded}
|
||||
className="p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 font-light text-secondary hover:text-link">
|
||||
- Config Overrides
|
||||
</h2>
|
||||
<div className="h-[calc(100vh_-_3.5rem_-_4rem)]">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
folding: false,
|
||||
renderLineHighlight: 'none',
|
||||
scrollBeyondLastLine: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Resizable>
|
||||
<button
|
||||
onClick={handleApplyConfig}
|
||||
title="Apply config overrides to input"
|
||||
aria-label="Apply config overrides to input"
|
||||
className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10 w-8 h-8 bg-blue-500 hover:bg-blue-600 text-white rounded-full border-2 border-white shadow-lg flex items-center justify-center text-sm font-medium transition-colors duration-150">
|
||||
→
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title="Expand config editor"
|
||||
aria-label="Expand config editor"
|
||||
style={{
|
||||
transform: 'rotate(90deg) translate(-50%)',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
onClick={toggleExpanded}
|
||||
className="flex-grow-0 w-5 transition-colors duration-150 ease-in font-light text-secondary hover:text-link">
|
||||
Config Overrides
|
||||
</button>
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350}}
|
||||
enable={{right: true, bottom: false}}>
|
||||
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
|
||||
<div
|
||||
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
|
||||
title="Minimize config editor"
|
||||
onClick={() => onToggle(false)}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
right: '-32px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="left" className="text-blue-50" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 flex flex-col m-2 mb-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Config Overrides
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
loading={''}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col m-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Applied Configs
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedOptions}
|
||||
loading={''}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
readOnly: true,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsedEditor({
|
||||
onToggle,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className="w-4 !h-[calc(100vh_-_3.5rem)]"
|
||||
style={{position: 'relative'}}>
|
||||
<div
|
||||
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
|
||||
title="Expand config editor"
|
||||
onClick={() => onToggle(true)}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
left: '-8px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="right" className="text-blue-50" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,21 +22,10 @@ import BabelPluginReactCompiler, {
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import clsx from 'clsx';
|
||||
import invariant from 'invariant';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {useDeferredValue, useMemo} from 'react';
|
||||
import {useMountEffect} from '../../hooks';
|
||||
import {defaultStore} from '../../lib/defaultStore';
|
||||
import {
|
||||
createMessage,
|
||||
initStoreFromUrlOrLocalStorage,
|
||||
MessageLevel,
|
||||
MessageSource,
|
||||
type Store,
|
||||
} from '../../lib/stores';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {useStore} from '../StoreContext';
|
||||
import ConfigEditor from './ConfigEditor';
|
||||
import Input from './Input';
|
||||
import {
|
||||
@@ -46,8 +35,6 @@ import {
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
import {useSearchParams} from 'next/navigation';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -144,10 +131,65 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
function parseOptions(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): PluginOptions {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
|
||||
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
|
||||
// Parse config overrides from config editor
|
||||
let configOverrideOptions: any = {};
|
||||
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
|
||||
if (configOverrides.trim()) {
|
||||
if (configMatch && configMatch[1]) {
|
||||
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
|
||||
configOverrideOptions = new Function(`return (${configString})`)();
|
||||
} else {
|
||||
throw new Error('Invalid override format');
|
||||
}
|
||||
}
|
||||
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedPragmaOptions,
|
||||
...configOverrideOptions,
|
||||
environment: {
|
||||
...parsedPragmaOptions.environment,
|
||||
...configOverrideOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
});
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
@@ -166,104 +208,94 @@ function compile(
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
try {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const parsedOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedOptions,
|
||||
environment: {
|
||||
...parsedOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent) => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Config,
|
||||
reason: `Unexpected failure when transforming configs! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (baseOpts) {
|
||||
try {
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Add logger options to the parsed options
|
||||
const opts = {
|
||||
...baseOpts,
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
} catch (err) {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
console.error(err);
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: `Unexpected failure when transforming input! ${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: `Unexpected failure when transforming input! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
@@ -271,59 +303,27 @@ function compile(
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language];
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
const store = useStore();
|
||||
const deferredStore = useDeferredValue(store);
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [compilerOutput, language] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler'),
|
||||
[deferredStore.source],
|
||||
const [compilerOutput, language, appliedOptions] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
);
|
||||
const [linterOutput] = useMemo(
|
||||
() => compile(deferredStore.source, 'linter'),
|
||||
[deferredStore.source],
|
||||
() => compile(deferredStore.source, 'linter', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
);
|
||||
|
||||
// TODO: Remove this once the config editor is more stable
|
||||
const searchParams = useSearchParams();
|
||||
const search = searchParams.get('showConfig');
|
||||
const shouldShowConfig = search === 'true';
|
||||
|
||||
useMountEffect(() => {
|
||||
// Initialize store
|
||||
let mountStore: Store;
|
||||
try {
|
||||
mountStore = initStoreFromUrlOrLocalStorage();
|
||||
} catch (e) {
|
||||
invariant(e instanceof Error, 'Only Error may be caught.');
|
||||
enqueueSnackbar(e.message, {
|
||||
variant: 'warning',
|
||||
...createMessage(
|
||||
'Bad URL - fell back to the default Playground.',
|
||||
MessageLevel.Info,
|
||||
MessageSource.Playground,
|
||||
),
|
||||
});
|
||||
mountStore = defaultStore;
|
||||
}
|
||||
|
||||
dispatchStore({
|
||||
type: 'setStore',
|
||||
payload: {
|
||||
store: mountStore,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
let mergedOutput: CompilerOutput;
|
||||
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
if (compilerOutput.kind === 'ok') {
|
||||
@@ -338,13 +338,17 @@ export default function Editor(): JSX.Element {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex basis top-14">
|
||||
{shouldShowConfig && <ConfigEditor />}
|
||||
<div className={clsx('relative sm:basis-1/4')}>
|
||||
<Input language={language} errors={errors} />
|
||||
<div className="relative flex top-14">
|
||||
<div className="flex-shrink-0">
|
||||
<ConfigEditor appliedOptions={appliedOptions} />
|
||||
</div>
|
||||
<div className={clsx('flex sm:flex flex-wrap')}>
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
<div className="flex flex-1 min-w-0">
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Input language={language} errors={errors} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,23 +6,25 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
|
||||
import {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import invariant from 'invariant';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
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});
|
||||
|
||||
type Props = {
|
||||
errors: Array<CompilerErrorDetail>;
|
||||
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
language: 'flow' | 'typescript';
|
||||
};
|
||||
|
||||
@@ -43,11 +45,6 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
details: errors,
|
||||
source: store.source,
|
||||
});
|
||||
/**
|
||||
* N.B. that `tabSize` is a model property, not an editor property.
|
||||
* So, the tab size has to be set per model.
|
||||
*/
|
||||
model.updateOptions({tabSize: 2});
|
||||
}, [monaco, errors, store.source]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -83,14 +80,10 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
const handleChange: (value: string | undefined) => void = async value => {
|
||||
if (!value) return;
|
||||
|
||||
// Parse and format the config
|
||||
const config = await parseAndFormatConfig(value);
|
||||
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
type: 'updateSource',
|
||||
payload: {
|
||||
source: value,
|
||||
config,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -140,30 +133,37 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const editorContent = (
|
||||
<MonacoEditor
|
||||
path={'index.js'}
|
||||
/**
|
||||
* .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
* check their syntax using its TS language service. They are still JS files
|
||||
* due to their extensions, so TS language features don't work.
|
||||
*/
|
||||
language={'javascript'}
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={monacoOptions}
|
||||
loading={''}
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = new Map([['Input', editorContent]]);
|
||||
const [activeTab, setActiveTab] = useState('Input');
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col flex-none border-r border-gray-200">
|
||||
<Resizable
|
||||
minWidth={650}
|
||||
enable={{right: true}}
|
||||
/**
|
||||
* Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
* will grow the editor to fit within parent element
|
||||
*/
|
||||
className="!h-[calc(100vh_-_3.5rem)]">
|
||||
<MonacoEditor
|
||||
path={'index.js'}
|
||||
/**
|
||||
* .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
* check their syntax using its TS language service. They are still JS files
|
||||
* due to their extensions, so TS language features don't work.
|
||||
*/
|
||||
language={'javascript'}
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
options={monacoOptions}
|
||||
/>
|
||||
</Resizable>
|
||||
<div className="!h-[calc(100vh_-_3.5rem)]">
|
||||
<div className="flex flex-col h-full">
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,17 @@ import * as prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {memo, ReactNode, useEffect, useState} from 'react';
|
||||
import {type Store} from '../../lib/stores';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
|
||||
const MemoizedOutput = memo(Output);
|
||||
|
||||
export default MemoizedOutput;
|
||||
|
||||
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'hir';
|
||||
@@ -71,7 +75,7 @@ async function tabify(
|
||||
const concattedResults = new Map<string, string>();
|
||||
// Concat all top level function declaration results into a single tab for each pass
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
if (!showInternals && passName !== 'Output' && passName !== 'SourceMap') {
|
||||
if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
|
||||
continue;
|
||||
}
|
||||
for (const result of results) {
|
||||
@@ -215,6 +219,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<string>('Output');
|
||||
|
||||
/*
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
@@ -226,6 +231,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
setTabsOpen(new Set(['Output']));
|
||||
setActiveTab('Output');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -249,16 +255,24 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
if (!store.showInternals) {
|
||||
return (
|
||||
<TabbedWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccordionWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,6 +324,7 @@ function TextTabContent({
|
||||
<DiffEditor
|
||||
original={diff}
|
||||
modified={output}
|
||||
loading={''}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
readOnly: true,
|
||||
@@ -324,6 +339,7 @@ function TextTabContent({
|
||||
<MonacoEditor
|
||||
language={language ?? 'javascript'}
|
||||
value={output}
|
||||
loading={''}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
readOnly: true,
|
||||
|
||||
@@ -29,4 +29,6 @@ export const monacoOptions: Partial<EditorProps['options']> = {
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
|
||||
tabSize: 2,
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function Header(): JSX.Element {
|
||||
'before:bg-white before:rounded-full before:transition-transform before:duration-250',
|
||||
'focus-within:shadow-[0_0_1px_#2196F3]',
|
||||
store.showInternals
|
||||
? 'bg-blue-500 before:translate-x-3.5'
|
||||
? 'bg-link before:translate-x-3.5'
|
||||
: 'bg-gray-300',
|
||||
)}></span>
|
||||
</label>
|
||||
|
||||
41
compiler/apps/playground/components/Icons/IconChevron.tsx
Normal file
41
compiler/apps/playground/components/Icons/IconChevron.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 {memo} from 'react';
|
||||
|
||||
export const IconChevron = memo<
|
||||
JSX.IntrinsicElements['svg'] & {
|
||||
/**
|
||||
* The direction the arrow should point.
|
||||
*/
|
||||
displayDirection: 'right' | 'left';
|
||||
}
|
||||
>(function IconChevron({className, displayDirection, ...props}) {
|
||||
const rotationClass =
|
||||
displayDirection === 'left' ? 'rotate-90' : '-rotate-90';
|
||||
const classes = className ? `${rotationClass} ${className}` : rotationClass;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={classes}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
{...props}>
|
||||
<g fill="none" fillRule="evenodd" transform="translate(-446 -398)">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
d="M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z"
|
||||
transform="translate(356.5 164.5)"
|
||||
/>
|
||||
<polygon points="446 418 466 418 466 398 446 398" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -6,10 +6,14 @@
|
||||
*/
|
||||
|
||||
import type {Dispatch, ReactNode} from 'react';
|
||||
import {useEffect, useReducer} from 'react';
|
||||
import {useState, useEffect, useReducer} from 'react';
|
||||
import createContext from '../lib/createContext';
|
||||
import {emptyStore} from '../lib/defaultStore';
|
||||
import {saveStore, type Store} from '../lib/stores';
|
||||
import {emptyStore, defaultStore} from '../lib/defaultStore';
|
||||
import {
|
||||
saveStore,
|
||||
initStoreFromUrlOrLocalStorage,
|
||||
type Store,
|
||||
} from '../lib/stores';
|
||||
|
||||
const StoreContext = createContext<Store>();
|
||||
|
||||
@@ -30,6 +34,20 @@ export const useStoreDispatch = StoreDispatchContext.useContext;
|
||||
*/
|
||||
export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
|
||||
const [store, dispatch] = useReducer(storeReducer, emptyStore);
|
||||
const [isPageReady, setIsPageReady] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let mountStore: Store;
|
||||
try {
|
||||
mountStore = initStoreFromUrlOrLocalStorage();
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize store from URL or local storage', e);
|
||||
mountStore = defaultStore;
|
||||
}
|
||||
dispatch({type: 'setStore', payload: {store: mountStore}});
|
||||
setIsPageReady(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (store !== emptyStore) {
|
||||
saveStore(store);
|
||||
@@ -39,7 +57,7 @@ export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
|
||||
return (
|
||||
<StoreContext.Provider value={store}>
|
||||
<StoreDispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
{isPageReady ? children : null}
|
||||
</StoreDispatchContext.Provider>
|
||||
</StoreContext.Provider>
|
||||
);
|
||||
@@ -53,9 +71,14 @@ type ReducerAction =
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateFile';
|
||||
type: 'updateSource';
|
||||
payload: {
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateConfig';
|
||||
payload: {
|
||||
config: string;
|
||||
};
|
||||
}
|
||||
@@ -69,11 +92,18 @@ function storeReducer(store: Store, action: ReducerAction): Store {
|
||||
const newStore = action.payload.store;
|
||||
return newStore;
|
||||
}
|
||||
case 'updateFile': {
|
||||
const {source, config} = action.payload;
|
||||
case 'updateSource': {
|
||||
const source = action.payload.source;
|
||||
const newStore = {
|
||||
...store,
|
||||
source,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
case 'updateConfig': {
|
||||
const config = action.payload.config;
|
||||
const newStore = {
|
||||
...store,
|
||||
config,
|
||||
};
|
||||
return newStore;
|
||||
|
||||
@@ -4,103 +4,40 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {Resizable} from 're-resizable';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
type TabsRecord = Map<string, React.ReactNode>;
|
||||
|
||||
export default function TabbedWindow(props: {
|
||||
defaultTab: string | null;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
}): React.ReactElement {
|
||||
if (props.tabs.size === 0) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{width: 'calc(100vw - 650px)'}}>
|
||||
No compiler output detected, see errors below
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<TabbedWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabbedWindowItem({
|
||||
name,
|
||||
export default function TabbedWindow({
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
tabs: Map<string, React.ReactNode>;
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
}): React.ReactElement {
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const toggleTabs = useCallback(() => {
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
}, [tabsOpen, name, setTabsOpen]);
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col h-full max-w-full">
|
||||
<div className="flex p-2 flex-shrink-0">
|
||||
{Array.from(tabs.keys()).map(tab => {
|
||||
const isActive = activeTab === tab;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => onTabChange(tab)}
|
||||
className={clsx(
|
||||
'active:scale-95 transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm',
|
||||
!isActive && 'hover:bg-primary/5',
|
||||
isActive && 'bg-highlight text-link',
|
||||
)}>
|
||||
{tab}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(activeTab)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* 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 {parsePluginOptions} from 'babel-plugin-react-compiler';
|
||||
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
|
||||
|
||||
export class ConfigError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ConfigError';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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 = `\
|
||||
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
(${configString} satisfies Partial<PluginOptions>)`;
|
||||
}
|
||||
|
||||
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('({') + 1;
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a config string can be parsed as a valid PluginOptions object
|
||||
* Throws an error if validation fails.
|
||||
*/
|
||||
function validateConfigAsPluginOptions(configString: string): void {
|
||||
// Validate that config can be parse as JS obj
|
||||
let parsedConfig: unknown;
|
||||
try {
|
||||
parsedConfig = new Function(`return (${configString})`)();
|
||||
} catch (_) {
|
||||
throw new ConfigError('Config has invalid syntax.');
|
||||
}
|
||||
|
||||
// Validate against PluginOptions schema
|
||||
try {
|
||||
parsePluginOptions(parsedConfig);
|
||||
} catch (_) {
|
||||
throw new ConfigError('Config does not match the expected schema.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
validateConfigAsPluginOptions(cleanConfig);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -17,22 +17,7 @@ export const defaultConfig = `\
|
||||
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
compilationMode: 'infer',
|
||||
panicThreshold: 'none',
|
||||
environment: {},
|
||||
logger: null,
|
||||
gating: null,
|
||||
noEmit: false,
|
||||
dynamicGating: null,
|
||||
eslintSuppressionRules: null,
|
||||
flowSuppressions: true,
|
||||
ignoreUseNoForget: false,
|
||||
sources: filename => {
|
||||
return filename.indexOf('node_modules') === -1;
|
||||
},
|
||||
enableReanimatedCheck: true,
|
||||
customOptOutDirectives: null,
|
||||
target: '19',
|
||||
//compilationMode: "all"
|
||||
} satisfies Partial<PluginOptions>);`;
|
||||
|
||||
export const defaultStore: Store = {
|
||||
|
||||
@@ -71,7 +71,7 @@ export function initStoreFromUrlOrLocalStorage(): Store {
|
||||
// Make sure all properties are populated
|
||||
return {
|
||||
source: raw.source,
|
||||
config: 'config' in raw ? raw.config : defaultConfig,
|
||||
config: 'config' in raw && raw['config'] ? raw.config : defaultConfig,
|
||||
showInternals: 'showInternals' in raw ? raw.showInternals : false,
|
||||
};
|
||||
}
|
||||
|
||||
2
compiler/apps/playground/next-env.d.ts
vendored
2
compiler/apps/playground/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
import "./.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,7 +34,7 @@
|
||||
"invariant": "^2.2.4",
|
||||
"lz-string": "^1.5.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"next": "15.5.2",
|
||||
"next": "15.6.0-canary.7",
|
||||
"notistack": "^3.0.0-alpha.7",
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-format": "^29.3.1",
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
|
||||
@@ -55,12 +55,16 @@ export default defineConfig({
|
||||
// contextOptions: {
|
||||
// ignoreHTTPSErrors: true,
|
||||
// },
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {...devices['Desktop Chrome']},
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'Desktop Firefox',
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"types": [
|
||||
"react/experimental"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -16,7 +19,7 @@
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -715,10 +715,10 @@
|
||||
dependencies:
|
||||
"@monaco-editor/loader" "^1.4.0"
|
||||
|
||||
"@next/env@15.5.2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.2.tgz#0c6b959313cd6e71afb69bf0deb417237f1d2f8a"
|
||||
integrity sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==
|
||||
"@next/env@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.6.0-canary.7.tgz#cdbf2967a9437ef09eef755e203f315acc4d8d8f"
|
||||
integrity sha512-LNZ7Yd3Cl9rKvjYdeJmszf2HmSDP76SQmfafKep2Ux16ZXKoN5OjwVHFTltKNdsB3vt2t+XJzLP2rhw5lBoFBA==
|
||||
|
||||
"@next/eslint-plugin-next@15.5.2":
|
||||
version "15.5.2"
|
||||
@@ -727,45 +727,45 @@
|
||||
dependencies:
|
||||
fast-glob "3.3.1"
|
||||
|
||||
"@next/swc-darwin-arm64@15.5.2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz#f69713326fc08f2eff3726fe19165cdb429d67c7"
|
||||
integrity sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==
|
||||
"@next/swc-darwin-arm64@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.6.0-canary.7.tgz#628cd34ce9120000f1cb5b08963426431174fc57"
|
||||
integrity sha512-POsBrxhrR3qvqXV+JZ6ZoBc8gJf8rhYe+OedceI1piPVqtJYOJa3EB4eaqcc+kMsllKRrH/goNlhLwtyhE+0Qg==
|
||||
|
||||
"@next/swc-darwin-x64@15.5.2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz#560a9da4126bae75cbbd6899646ad7a2e4fdcc9b"
|
||||
integrity sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==
|
||||
"@next/swc-darwin-x64@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.6.0-canary.7.tgz#37d4ebab14da74a2f8028daf6d76aab410153e06"
|
||||
integrity sha512-lmk9ysBuSiPlAJZTCo/3O4mXNFosg6EDIf4GrmynIwCG2as6/KxzyD1WqFp56Exp8eFDjP7SFapD10sV43vCsA==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@15.5.2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz#80b2be276e775e5a9286369ae54e536b0cdf8c3a"
|
||||
integrity sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==
|
||||
"@next/swc-linux-arm64-gnu@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.6.0-canary.7.tgz#ce700cc0e0d24763136838223105a524b36694fa"
|
||||
integrity sha512-why8k6d0SBm3AKoOD5S7ir3g+BF34l9oFKIoZrLaZaKBvNGpFcjc7Ovc2TunNMeaMJzv9k1dHYSap0EI5oSuzg==
|
||||
|
||||
"@next/swc-linux-arm64-musl@15.5.2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz#68cf676301755fd99aca11a7ebdb5eae88d7c2e4"
|
||||
integrity sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==
|
||||
"@next/swc-linux-arm64-musl@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.6.0-canary.7.tgz#c791b8e15bf2c338b4cc0387fe7afb3ef83ecfcf"
|
||||
integrity sha512-HzvTRsKvYj32Va4YuJN3n3xOxvk+6QwB63d/EsgmdkeA/vrqciUAmJDYpuzZEvRc3Yp2nyPq8KZxtHAr6ISZ2Q==
|
||||
|
||||
"@next/swc-linux-x64-gnu@15.5.2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz#209d9a79d0f2333544f863b0daca3f7e29f2eaff"
|
||||
integrity sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==
|
||||
"@next/swc-linux-x64-gnu@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.6.0-canary.7.tgz#c01c3a3d8e71660c49298dd053d078379b6b5919"
|
||||
integrity sha512-6yRFrg2qWXOqa+1BI53J9EmHWFzKg9U2r+5R7n7BFUp8PH5SC92WBsmYTnh/RkvAYvdupiVzMervwwswCs6kFg==
|
||||
|
||||
"@next/swc-linux-x64-musl@15.5.2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz#d4ad1cfb5e99e51db669fe2145710c1abeadbd7f"
|
||||
integrity sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==
|
||||
"@next/swc-linux-x64-musl@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.6.0-canary.7.tgz#3f4b39faef4a5f88b13e4c726b008ddc9717f819"
|
||||
integrity sha512-O/JjvOvNK/Wao/OIQaA6evDkxkmFFQgJ1/hI1dVk6/PAeKmW2/Q+6Dodh97eAkOwedS1ZdQl2mojf87TzLvzdQ==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@15.5.2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz#070e10e370a5447a198c2db100389646aca2c496"
|
||||
integrity sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==
|
||||
"@next/swc-win32-arm64-msvc@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.6.0-canary.7.tgz#9bc5da0907b7ce67eedda02a6d56a09d9a539ccf"
|
||||
integrity sha512-p9DvrDgnePofZCtiWVY7qZtwXxiOGJlAyy2LoGPYSGOUDhjbTG8j6XMUFXpV9UwpH+l7st522psO1BVzbpT8IQ==
|
||||
|
||||
"@next/swc-win32-x64-msvc@15.5.2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz#9237d40b82eaf2efc88baeba15b784d4126caf4a"
|
||||
integrity sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==
|
||||
"@next/swc-win32-x64-msvc@15.6.0-canary.7":
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.6.0-canary.7.tgz#5b271c591ccbe67d5fa966dd22db86c547414fd1"
|
||||
integrity sha512-f1ywT3xWu4StWKA1mZRyGfelu/h+W0OEEyBxQNXzXyYa0VGZb9LyCNb5cYoNKBm0Bw18Hp1PVe0bHuusemGCcw==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -866,6 +866,13 @@
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@19.1.13":
|
||||
version "19.1.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883"
|
||||
integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz#9c8218ed62f9a322df10ded7c34990f014df44f2"
|
||||
@@ -3199,25 +3206,25 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next@15.5.2:
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.5.2.tgz#5e50102443fb0328a9dfcac2d82465c7bac93693"
|
||||
integrity sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==
|
||||
next@15.6.0-canary.7:
|
||||
version "15.6.0-canary.7"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.6.0-canary.7.tgz#bfc2ac3c9a78e23d550c303d18247a263e6b5bc1"
|
||||
integrity sha512-4ukX2mxat9wWT6E0Gw/3TOR9ULV1q399E42F86cwsPSFgTWa04ABhcTqO0r9J/QR1YWPR8WEgh9qUzmWA/1yEw==
|
||||
dependencies:
|
||||
"@next/env" "15.5.2"
|
||||
"@next/env" "15.6.0-canary.7"
|
||||
"@swc/helpers" "0.5.15"
|
||||
caniuse-lite "^1.0.30001579"
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.6"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "15.5.2"
|
||||
"@next/swc-darwin-x64" "15.5.2"
|
||||
"@next/swc-linux-arm64-gnu" "15.5.2"
|
||||
"@next/swc-linux-arm64-musl" "15.5.2"
|
||||
"@next/swc-linux-x64-gnu" "15.5.2"
|
||||
"@next/swc-linux-x64-musl" "15.5.2"
|
||||
"@next/swc-win32-arm64-msvc" "15.5.2"
|
||||
"@next/swc-win32-x64-msvc" "15.5.2"
|
||||
"@next/swc-darwin-arm64" "15.6.0-canary.7"
|
||||
"@next/swc-darwin-x64" "15.6.0-canary.7"
|
||||
"@next/swc-linux-arm64-gnu" "15.6.0-canary.7"
|
||||
"@next/swc-linux-arm64-musl" "15.6.0-canary.7"
|
||||
"@next/swc-linux-x64-gnu" "15.6.0-canary.7"
|
||||
"@next/swc-linux-x64-musl" "15.6.0-canary.7"
|
||||
"@next/swc-win32-arm64-msvc" "15.6.0-canary.7"
|
||||
"@next/swc-win32-x64-msvc" "15.6.0-canary.7"
|
||||
sharp "^0.34.3"
|
||||
|
||||
node-releases@^2.0.18:
|
||||
|
||||
@@ -276,7 +276,7 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir));
|
||||
env.logErrors(validateNoSetStateInEffects(hir, env));
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
|
||||
@@ -3081,6 +3081,12 @@ function isReorderableExpression(
|
||||
return true;
|
||||
}
|
||||
}
|
||||
case 'TSInstantiationExpression': {
|
||||
const innerExpr = (expr as NodePath<t.TSInstantiationExpression>).get(
|
||||
'expression',
|
||||
) as NodePath<t.Expression>;
|
||||
return isReorderableExpression(builder, innerExpr, allowLocalIdentifiers);
|
||||
}
|
||||
case 'RegExpLiteral':
|
||||
case 'StringLiteral':
|
||||
case 'NumericLiteral':
|
||||
|
||||
@@ -86,6 +86,24 @@ export function defaultModuleTypeProvider(
|
||||
},
|
||||
};
|
||||
}
|
||||
case '@tanstack/react-virtual': {
|
||||
return {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
/*
|
||||
* Many of the properties of `useVirtualizer()`'s return value are incompatible, so we mark the entire hook
|
||||
* as incompatible
|
||||
*/
|
||||
useVirtualizer: {
|
||||
kind: 'hook',
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
knownIncompatible: `TanStack Virtual's \`useVirtualizer()\` API returns functions that cannot be memoized safely`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -621,6 +621,13 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Treat identifiers as SetState type if both
|
||||
* - they are named with a "set-" prefix
|
||||
* - they are called somewhere
|
||||
*/
|
||||
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* If specified a value, the compiler lowers any calls to `useContext` to use
|
||||
* this value as the callee.
|
||||
@@ -660,6 +667,13 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* while its parent function remains uncompiled.
|
||||
*/
|
||||
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When enabled, allows setState calls in effects when the value being set is
|
||||
* derived from a ref. This is useful for patterns where initial layout measurements
|
||||
* from refs need to be stored in state during mount.
|
||||
*/
|
||||
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
|
||||
@@ -748,10 +748,14 @@ function applyEffect(
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'MaybeAlias' ||
|
||||
initialized.has(effect.into.identifier.id),
|
||||
{
|
||||
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
|
||||
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
|
||||
reason: `Expected destination to already be initialized within this instruction`,
|
||||
description:
|
||||
`Destination ${printPlace(effect.into)} is not initialized in this ` +
|
||||
`instruction for effect ${printAliasingEffect(effect)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
@@ -767,49 +771,67 @@ function applyEffect(
|
||||
* copy-on-write semantics, then we can prune the effect
|
||||
*/
|
||||
const intoKind = state.kind(effect.into).kind;
|
||||
let isMutableDesination: boolean;
|
||||
let destinationType: 'context' | 'mutable' | null = null;
|
||||
switch (intoKind) {
|
||||
case ValueKind.Context:
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
isMutableDesination = true;
|
||||
case ValueKind.Context: {
|
||||
destinationType = 'context';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableDesination = false;
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
destinationType = 'mutable';
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fromKind = state.kind(effect.from).kind;
|
||||
let isMutableReferenceType: boolean;
|
||||
let sourceType: 'context' | 'mutable' | 'frozen' | null = null;
|
||||
switch (fromKind) {
|
||||
case ValueKind.Context: {
|
||||
sourceType = 'context';
|
||||
break;
|
||||
}
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Primitive: {
|
||||
isMutableReferenceType = false;
|
||||
break;
|
||||
}
|
||||
case ValueKind.Frozen: {
|
||||
isMutableReferenceType = false;
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
sourceType = 'frozen';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableReferenceType = true;
|
||||
sourceType = 'mutable';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isMutableDesination && isMutableReferenceType) {
|
||||
|
||||
if (sourceType === 'frozen') {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
} else if (
|
||||
(sourceType === 'mutable' && destinationType === 'mutable') ||
|
||||
effect.kind === 'MaybeAlias'
|
||||
) {
|
||||
effects.push(effect);
|
||||
} else if (
|
||||
(sourceType === 'context' && destinationType != null) ||
|
||||
(sourceType === 'mutable' && destinationType === 'context')
|
||||
) {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{kind: 'MaybeAlias', from: effect.from, into: effect.into},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1794,8 +1816,16 @@ function computeSignatureForInstruction(
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
/**
|
||||
* Add a hint about naming as "ref"/"-Ref", but only if we weren't able to infer any
|
||||
* type for the object. In some cases the variable may be named like a ref, but is
|
||||
* also used as a ref callback such that we infer the type as a function rather than
|
||||
* a ref.
|
||||
*/
|
||||
const mutationReason: MutationReason | null =
|
||||
value.kind === 'PropertyStore' && value.property === 'current'
|
||||
value.kind === 'PropertyStore' &&
|
||||
value.property === 'current' &&
|
||||
value.object.identifier.type.kind === 'Type'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
|
||||
@@ -779,7 +779,13 @@ class AliasingState {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
|
||||
queue.push({
|
||||
place: edge.node,
|
||||
transitive,
|
||||
direction: 'forwards',
|
||||
// Traversing a maybeAlias edge always downgrades to conditional mutation
|
||||
kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,
|
||||
});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -807,7 +813,12 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards', kind});
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* MaybeAlias indicates potential data flow from unknown function calls,
|
||||
|
||||
@@ -19,7 +19,7 @@ export function nameAnonymousFunctions(fn: HIRFunction): void {
|
||||
const parentName = fn.id;
|
||||
const functions = nameAnonymousFunctionsImpl(fn);
|
||||
function visit(node: Node, prefix: string): void {
|
||||
if (node.generatedName != null) {
|
||||
if (node.generatedName != null && node.fn.nameHint == null) {
|
||||
/**
|
||||
* Note that we don't generate a name for functions that already had one,
|
||||
* so we'll only add the prefix to anonymous functions regardless of
|
||||
@@ -70,6 +70,10 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
if (name != null && name.kind === 'named') {
|
||||
names.set(lvalue.identifier.id, name.value);
|
||||
}
|
||||
const func = functions.get(value.place.identifier.id);
|
||||
if (func != null) {
|
||||
functions.set(lvalue.identifier.id, func);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
@@ -106,6 +110,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
const variableName = value.lvalue.place.identifier.name;
|
||||
if (
|
||||
node != null &&
|
||||
node.generatedName == null &&
|
||||
variableName != null &&
|
||||
variableName.kind === 'named'
|
||||
) {
|
||||
@@ -137,7 +142,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(arg.identifier.id);
|
||||
if (node != null) {
|
||||
if (node != null && node.generatedName == null) {
|
||||
const generatedName =
|
||||
fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
|
||||
node.generatedName = generatedName;
|
||||
@@ -152,7 +157,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(attr.place.identifier.id);
|
||||
if (node != null) {
|
||||
if (node != null && node.generatedName == null) {
|
||||
const elementName =
|
||||
value.tag.kind === 'BuiltinTag'
|
||||
? value.tag.name
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
BuiltInObjectId,
|
||||
BuiltInPropsId,
|
||||
BuiltInRefValueId,
|
||||
BuiltInSetStateId,
|
||||
BuiltInUseRefId,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
@@ -276,9 +277,16 @@ function* generateInstructionTypes(
|
||||
* We should change Hook to a subtype of Function or change unifier logic.
|
||||
* (see https://github.com/facebook/react-forget/pull/1427)
|
||||
*/
|
||||
let shapeId: string | null = null;
|
||||
if (env.config.enableTreatSetIdentifiersAsStateSetters) {
|
||||
const name = getName(names, value.callee.identifier.id);
|
||||
if (name.startsWith('set')) {
|
||||
shapeId = BuiltInSetStateId;
|
||||
}
|
||||
}
|
||||
yield equation(value.callee.identifier.type, {
|
||||
kind: 'Function',
|
||||
shapeId: null,
|
||||
shapeId,
|
||||
return: returnType,
|
||||
isConstructor: false,
|
||||
});
|
||||
|
||||
@@ -188,11 +188,6 @@ export function parseConfigPragmaForTests(
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
const overridePragma = parseConfigPragmaAsString(pragma);
|
||||
if (overridePragma !== '') {
|
||||
return parseConfigStringAsJS(overridePragma, defaults);
|
||||
}
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
pragma,
|
||||
defaults.environment ?? {},
|
||||
@@ -228,100 +223,3 @@ export function parseConfigPragmaForTests(
|
||||
}
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
|
||||
export function parseConfigPragmaAsString(pragma: string): string {
|
||||
// Check if it's in JS override format
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (key === 'OVERRIDE' && val != null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function parseConfigStringAsJS(
|
||||
configString: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
let parsedConfig: any;
|
||||
try {
|
||||
// Parse the JavaScript object literal
|
||||
parsedConfig = new Function(`return ${configString}`)();
|
||||
} catch (error) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Failed to parse config pragma as JavaScript object',
|
||||
description: `Could not parse: ${configString}. Error: ${error}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
'',
|
||||
defaults.environment ?? {},
|
||||
);
|
||||
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
compilationMode: defaults.compilationMode,
|
||||
environment,
|
||||
};
|
||||
|
||||
// Apply parsed config, merging environment if it exists
|
||||
if (parsedConfig.environment) {
|
||||
const mergedEnvironment = {
|
||||
...(options.environment as Record<string, unknown>),
|
||||
...parsedConfig.environment,
|
||||
};
|
||||
|
||||
// Validate environment config
|
||||
const validatedEnvironment =
|
||||
EnvironmentConfigSchema.safeParse(mergedEnvironment);
|
||||
if (!validatedEnvironment.success) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Invalid environment configuration in config pragma',
|
||||
description: `${fromZodError(validatedEnvironment.error)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
options.environment = validatedEnvironment.data;
|
||||
}
|
||||
|
||||
// Apply other config options
|
||||
for (const [key, value] of Object.entries(parsedConfig)) {
|
||||
if (key === 'environment') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasOwnProperty(defaultOptions, key)) {
|
||||
if (key === 'target' && value === 'donotuse_meta_internal') {
|
||||
options[key] = {
|
||||
kind: value,
|
||||
runtimeModule: 'react',
|
||||
};
|
||||
} else {
|
||||
options[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
|
||||
@@ -639,12 +639,55 @@ function validateNoRefAccessInRenderImpl(
|
||||
case 'StartMemoize':
|
||||
case 'FinishMemoize':
|
||||
break;
|
||||
case 'LoadGlobal': {
|
||||
if (instr.value.binding.name === 'undefined') {
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Primitive': {
|
||||
if (instr.value.value == null) {
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'UnaryExpression': {
|
||||
if (instr.value.operator === '!') {
|
||||
const value = env.get(instr.value.value.identifier.id);
|
||||
const refId =
|
||||
value?.kind === 'RefValue' && value.refId != null
|
||||
? value.refId
|
||||
: null;
|
||||
if (refId !== null) {
|
||||
/*
|
||||
* Record an error suggesting the `if (ref.current == null)` pattern,
|
||||
* but also record the lvalue as a guard so that we don't emit a second
|
||||
* error for the write to the ref
|
||||
*/
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
})
|
||||
.withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.value.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
})
|
||||
.withDetails({
|
||||
kind: 'hint',
|
||||
message:
|
||||
'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
validateNoRefValueAccess(errors, env, instr.value.value);
|
||||
break;
|
||||
}
|
||||
case 'BinaryExpression': {
|
||||
const left = env.get(instr.value.left.identifier.id);
|
||||
const right = env.get(instr.value.right.identifier.id);
|
||||
|
||||
@@ -11,16 +11,23 @@ import {
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
Environment,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
isUseRefType,
|
||||
isRefValueType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates against calling setState in the body of an effect (useEffect and friends),
|
||||
@@ -32,6 +39,7 @@ import {Result} from '../Utils/Result';
|
||||
*/
|
||||
export function validateNoSetStateInEffects(
|
||||
fn: HIRFunction,
|
||||
env: Environment,
|
||||
): Result<void, CompilerError> {
|
||||
const setStateFunctions: Map<IdentifierId, Place> = new Map();
|
||||
const errors = new CompilerError();
|
||||
@@ -72,6 +80,7 @@ export function validateNoSetStateInEffects(
|
||||
const callee = getSetStateCall(
|
||||
instr.value.loweredFunc.func,
|
||||
setStateFunctions,
|
||||
env,
|
||||
);
|
||||
if (callee !== null) {
|
||||
setStateFunctions.set(instr.lvalue.identifier.id, callee);
|
||||
@@ -129,9 +138,42 @@ export function validateNoSetStateInEffects(
|
||||
function getSetStateCall(
|
||||
fn: HIRFunction,
|
||||
setStateFunctions: Map<IdentifierId, Place>,
|
||||
env: Environment,
|
||||
): Place | null {
|
||||
const refDerivedValues: Set<IdentifierId> = new Set();
|
||||
|
||||
const isDerivedFromRef = (place: Place): boolean => {
|
||||
return (
|
||||
refDerivedValues.has(place.identifier.id) ||
|
||||
isUseRefType(place.identifier) ||
|
||||
isRefValueType(place.identifier)
|
||||
);
|
||||
};
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (env.config.enableAllowSetStateFromRefsInEffects) {
|
||||
const hasRefOperand = Iterable_some(
|
||||
eachInstructionValueOperand(instr.value),
|
||||
isDerivedFromRef,
|
||||
);
|
||||
|
||||
if (hasRefOperand) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
refDerivedValues.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'PropertyLoad' &&
|
||||
instr.value.property === 'current' &&
|
||||
(isUseRefType(instr.value.object.identifier) ||
|
||||
isRefValueType(instr.value.object.identifier))
|
||||
) {
|
||||
refDerivedValues.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
switch (instr.value.kind) {
|
||||
case 'LoadLocal': {
|
||||
if (setStateFunctions.has(instr.value.place.identifier.id)) {
|
||||
@@ -161,6 +203,21 @@ function getSetStateCall(
|
||||
isSetStateType(callee.identifier) ||
|
||||
setStateFunctions.has(callee.identifier.id)
|
||||
) {
|
||||
if (env.config.enableAllowSetStateFromRefsInEffects) {
|
||||
const arg = instr.value.args.at(0);
|
||||
if (
|
||||
arg !== undefined &&
|
||||
arg.kind === 'Identifier' &&
|
||||
refDerivedValues.has(arg.identifier.id)
|
||||
) {
|
||||
/**
|
||||
* The one special case where we allow setStates in effects is in the very specific
|
||||
* scenario where the value being set is derived from a ref. For example this may
|
||||
* be needed when initial layout measurements from refs need to be stored in state.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* TODO: once we support multiple locations per error, we should link to the
|
||||
* original Place in the case that setStateFunction.has(callee)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
if (r.current == undefined) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { useRef } from "react";
|
||||
|
||||
function C() {
|
||||
const r = useRef(null);
|
||||
if (r.current == undefined) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok)
|
||||
@@ -0,0 +1,14 @@
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
if (r.current == undefined) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
const current = !r.current;
|
||||
return <div>{current}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 4 errors:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | const current = !r.current;
|
||||
| ^^^^^^^^^ Cannot access ref value during render
|
||||
7 | return <div>{current}</div>;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | const current = !r.current;
|
||||
| ^^^^^^^^^^ Cannot access ref value during render
|
||||
7 | return <div>{current}</div>;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
5 | const r = useRef(null);
|
||||
6 | const current = !r.current;
|
||||
> 7 | return <div>{current}</div>;
|
||||
| ^^^^^^^ Cannot access ref value during render
|
||||
8 | }
|
||||
9 |
|
||||
10 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
5 | const r = useRef(null);
|
||||
6 | const current = !r.current;
|
||||
> 7 | return <div>{current}</div>;
|
||||
| ^^^^^^^ Cannot access ref value during render
|
||||
8 | }
|
||||
9 |
|
||||
10 | export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
const current = !r.current;
|
||||
return <div>{current}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
if (!r.current) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | if (!r.current) {
|
||||
| ^^^^^^^^^ Cannot access ref value during render
|
||||
7 | r.current = 1;
|
||||
8 | }
|
||||
9 | }
|
||||
|
||||
To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
if (!r.current) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component() {
|
||||
const [state, setState] = useCustomState(0);
|
||||
const aliased = setState;
|
||||
|
||||
setState(1);
|
||||
aliased(2);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function useCustomState(init) {
|
||||
return useState(init);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2
|
||||
4 | const aliased = setState;
|
||||
5 |
|
||||
> 6 | setState(1);
|
||||
| ^^^^^^^^ Found setState() in render
|
||||
7 | aliased(2);
|
||||
8 |
|
||||
9 | return state;
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-unconditional-set-state-hook-return-in-render.ts:7:2
|
||||
5 |
|
||||
6 | setState(1);
|
||||
> 7 | aliased(2);
|
||||
| ^^^^^^^ Found setState() in render
|
||||
8 |
|
||||
9 | return state;
|
||||
10 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component() {
|
||||
const [state, setState] = useCustomState(0);
|
||||
const aliased = setState;
|
||||
|
||||
setState(1);
|
||||
aliased(2);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function useCustomState(init) {
|
||||
return useState(init);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component({setX}) {
|
||||
const aliased = setX;
|
||||
|
||||
setX(1);
|
||||
aliased(2);
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-unconditional-set-state-prop-in-render.ts:5:2
|
||||
3 | const aliased = setX;
|
||||
4 |
|
||||
> 5 | setX(1);
|
||||
| ^^^^ Found setState() in render
|
||||
6 | aliased(2);
|
||||
7 |
|
||||
8 | return x;
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-unconditional-set-state-prop-in-render.ts:6:2
|
||||
4 |
|
||||
5 | setX(1);
|
||||
> 6 | aliased(2);
|
||||
| ^^^^^^^ Found setState() in render
|
||||
7 |
|
||||
8 | return x;
|
||||
9 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component({setX}) {
|
||||
const aliased = setX;
|
||||
|
||||
setX(1);
|
||||
aliased(2);
|
||||
|
||||
return x;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender
|
||||
|
||||
function useHook(parentRef) {
|
||||
// Some components accept a union of "callback" refs and ref objects, which
|
||||
// we can't currently represent
|
||||
const elementRef = useRef(null);
|
||||
const handler = instance => {
|
||||
elementRef.current = instance;
|
||||
if (parentRef != null) {
|
||||
if (typeof parentRef === 'function') {
|
||||
// This call infers the type of `parentRef` as a function...
|
||||
parentRef(instance);
|
||||
} else {
|
||||
// So this assignment fails since we don't know its a ref
|
||||
parentRef.current = instance;
|
||||
}
|
||||
}
|
||||
};
|
||||
return handler;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
|
||||
|
||||
error.todo-allow-assigning-to-inferred-ref-prop-in-callback.ts:15:8
|
||||
13 | } else {
|
||||
14 | // So this assignment fails since we don't know its a ref
|
||||
> 15 | parentRef.current = instance;
|
||||
| ^^^^^^^^^ `parentRef` cannot be modified
|
||||
16 | }
|
||||
17 | }
|
||||
18 | };
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @validateRefAccessDuringRender
|
||||
|
||||
function useHook(parentRef) {
|
||||
// Some components accept a union of "callback" refs and ref objects, which
|
||||
// we can't currently represent
|
||||
const elementRef = useRef(null);
|
||||
const handler = instance => {
|
||||
elementRef.current = instance;
|
||||
if (parentRef != null) {
|
||||
if (typeof parentRef === 'function') {
|
||||
// This call infers the type of `parentRef` as a function...
|
||||
parentRef(instance);
|
||||
} else {
|
||||
// So this assignment fails since we don't know its a ref
|
||||
parentRef.current = instance;
|
||||
}
|
||||
}
|
||||
};
|
||||
return handler;
|
||||
}
|
||||
@@ -4,15 +4,19 @@
|
||||
```javascript
|
||||
// @enableNameAnonymousFunctions
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
import * as SharedRuntime from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
function named() {
|
||||
const inner = () => props.named;
|
||||
return inner();
|
||||
const innerIdentity = identity(() => props.named);
|
||||
return inner(innerIdentity());
|
||||
}
|
||||
const callback = useCallback(() => {
|
||||
return 'ok';
|
||||
}, []);
|
||||
const namedVariable = function () {
|
||||
return props.namedVariable;
|
||||
};
|
||||
@@ -30,6 +34,7 @@ function Component(props) {
|
||||
return (
|
||||
<>
|
||||
{named()}
|
||||
{callback()}
|
||||
{namedVariable()}
|
||||
{methodCall()}
|
||||
{call()}
|
||||
@@ -63,7 +68,7 @@ export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { identity, Stringify, useIdentity } from "shared-runtime";
|
||||
import * as SharedRuntime from "shared-runtime";
|
||||
|
||||
@@ -75,7 +80,12 @@ function Component(props) {
|
||||
const inner = { "Component[named > inner]": () => props.named }[
|
||||
"Component[named > inner]"
|
||||
];
|
||||
return inner();
|
||||
const innerIdentity = identity(
|
||||
{ "Component[named > identity()]": () => props.named }[
|
||||
"Component[named > identity()]"
|
||||
],
|
||||
);
|
||||
return inner(innerIdentity());
|
||||
};
|
||||
$[0] = props.named;
|
||||
$[1] = t0;
|
||||
@@ -83,6 +93,8 @@ function Component(props) {
|
||||
t0 = $[1];
|
||||
}
|
||||
const named = t0;
|
||||
|
||||
const callback = _ComponentCallback;
|
||||
let t1;
|
||||
if ($[2] !== props.namedVariable) {
|
||||
t1 = {
|
||||
@@ -197,57 +209,62 @@ function Component(props) {
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
const t10 = callback();
|
||||
let t11;
|
||||
if ($[19] !== namedVariable) {
|
||||
t10 = namedVariable();
|
||||
t11 = namedVariable();
|
||||
$[19] = namedVariable;
|
||||
$[20] = t10;
|
||||
$[20] = t11;
|
||||
} else {
|
||||
t10 = $[20];
|
||||
}
|
||||
const t11 = methodCall();
|
||||
const t12 = call();
|
||||
let t13;
|
||||
if ($[21] !== hookArgument) {
|
||||
t13 = hookArgument();
|
||||
$[21] = hookArgument;
|
||||
$[22] = t13;
|
||||
} else {
|
||||
t13 = $[22];
|
||||
t11 = $[20];
|
||||
}
|
||||
const t12 = methodCall();
|
||||
const t13 = call();
|
||||
let t14;
|
||||
if ($[21] !== hookArgument) {
|
||||
t14 = hookArgument();
|
||||
$[21] = hookArgument;
|
||||
$[22] = t14;
|
||||
} else {
|
||||
t14 = $[22];
|
||||
}
|
||||
let t15;
|
||||
if (
|
||||
$[23] !== builtinElementAttr ||
|
||||
$[24] !== namedElementAttr ||
|
||||
$[25] !== t10 ||
|
||||
$[26] !== t11 ||
|
||||
$[27] !== t12 ||
|
||||
$[28] !== t13 ||
|
||||
$[25] !== t11 ||
|
||||
$[26] !== t12 ||
|
||||
$[27] !== t13 ||
|
||||
$[28] !== t14 ||
|
||||
$[29] !== t9
|
||||
) {
|
||||
t14 = (
|
||||
t15 = (
|
||||
<>
|
||||
{t9}
|
||||
{t10}
|
||||
{t11}
|
||||
{t12}
|
||||
{t13}
|
||||
{builtinElementAttr}
|
||||
{namedElementAttr}
|
||||
{t13}
|
||||
{t14}
|
||||
</>
|
||||
);
|
||||
$[23] = builtinElementAttr;
|
||||
$[24] = namedElementAttr;
|
||||
$[25] = t10;
|
||||
$[26] = t11;
|
||||
$[27] = t12;
|
||||
$[28] = t13;
|
||||
$[25] = t11;
|
||||
$[26] = t12;
|
||||
$[27] = t13;
|
||||
$[28] = t14;
|
||||
$[29] = t9;
|
||||
$[30] = t14;
|
||||
$[30] = t15;
|
||||
} else {
|
||||
t14 = $[30];
|
||||
t15 = $[30];
|
||||
}
|
||||
return t14;
|
||||
return t15;
|
||||
}
|
||||
function _ComponentCallback() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// @enableNameAnonymousFunctions
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
import * as SharedRuntime from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
function named() {
|
||||
const inner = () => props.named;
|
||||
return inner();
|
||||
const innerIdentity = identity(() => props.named);
|
||||
return inner(innerIdentity());
|
||||
}
|
||||
const callback = useCallback(() => {
|
||||
return 'ok';
|
||||
}, []);
|
||||
const namedVariable = function () {
|
||||
return props.namedVariable;
|
||||
};
|
||||
@@ -26,6 +30,7 @@ function Component(props) {
|
||||
return (
|
||||
<>
|
||||
{named()}
|
||||
{callback()}
|
||||
{namedVariable()}
|
||||
{methodCall()}
|
||||
{call()}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
function Component() {
|
||||
const dispatch = useDispatch();
|
||||
// const [state, setState] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={event => {
|
||||
dispatch(...event.target);
|
||||
event.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useDispatch() {
|
||||
'use no memo';
|
||||
// skip compilation to make it easier to debug the above function
|
||||
return (...values) => {
|
||||
console.log(...values);
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const dispatch = useDispatch();
|
||||
let t0;
|
||||
if ($[0] !== dispatch) {
|
||||
t0 = (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(event) => {
|
||||
dispatch(...event.target);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
$[0] = dispatch;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
function useDispatch() {
|
||||
"use no memo";
|
||||
// skip compilation to make it easier to debug the above function
|
||||
return (...values) => {
|
||||
console.log(...values);
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input type="file"></div>
|
||||
@@ -0,0 +1,30 @@
|
||||
// @compilationMode:"infer"
|
||||
function Component() {
|
||||
const dispatch = useDispatch();
|
||||
// const [state, setState] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={event => {
|
||||
dispatch(...event.target);
|
||||
event.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useDispatch() {
|
||||
'use no memo';
|
||||
// skip compilation to make it easier to debug the above function
|
||||
return (...values) => {
|
||||
console.log(...values);
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function id<T>(x: T): T {
|
||||
return x;
|
||||
}
|
||||
|
||||
export function Component<T = string>({fn = id<T>}: {fn?: (x: T) => T}) {
|
||||
const value = fn('hi' as T);
|
||||
return <div>{String(value)}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function id(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
export function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const { fn: t1 } = t0;
|
||||
const fn = t1 === undefined ? id : t1;
|
||||
let t2;
|
||||
if ($[0] !== fn) {
|
||||
t2 = fn("hi" as T);
|
||||
$[0] = fn;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const value = t2;
|
||||
const t3 = String(value);
|
||||
let t4;
|
||||
if ($[2] !== t3) {
|
||||
t4 = <div>{t3}</div>;
|
||||
$[2] = t3;
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>hi</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
function id<T>(x: T): T {
|
||||
return x;
|
||||
}
|
||||
|
||||
export function Component<T = string>({fn = id<T>}: {fn?: (x: T) => T}) {
|
||||
const value = fn('hi' as T);
|
||||
return <div>{String(value)}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const {height} = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
function Tooltip() {
|
||||
const $ = _c(2);
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
const { height } = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
};
|
||||
t1 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Cannot read properties of null (reading 'getBoundingClientRect')
|
||||
@@ -0,0 +1,19 @@
|
||||
// @validateNoSetStateInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const {height} = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useLayoutEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef({size: 5});
|
||||
const [computedSize, setComputedSize] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setComputedSize(ref.current.size * 10);
|
||||
}, []);
|
||||
|
||||
return computedSize;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = { size: 5 };
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const ref = useRef(t0);
|
||||
const [computedSize, setComputedSize] = useState(0);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
setComputedSize(ref.current.size * 10);
|
||||
};
|
||||
t2 = [];
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useLayoutEffect(t1, t2);
|
||||
return computedSize;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 50
|
||||
@@ -0,0 +1,18 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useLayoutEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef({size: 5});
|
||||
const [computedSize, setComputedSize] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setComputedSize(ref.current.size * 10);
|
||||
}, []);
|
||||
|
||||
return computedSize;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef([1, 2, 3, 4, 5]);
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const index = 2;
|
||||
setValue(ref.current[index]);
|
||||
}, []);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [1, 2, 3, 4, 5];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const ref = useRef(t0);
|
||||
const [value, setValue] = useState(0);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
setValue(ref.current[2]);
|
||||
};
|
||||
t2 = [];
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
return value;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 3
|
||||
@@ -0,0 +1,19 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef([1, 2, 3, 4, 5]);
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const index = 2;
|
||||
setValue(ref.current[index]);
|
||||
}, []);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
function getBoundingRect(ref) {
|
||||
if (ref.current) {
|
||||
return ref.current.getBoundingClientRect?.()?.width ?? 100;
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
|
||||
setWidth(getBoundingRect(ref));
|
||||
}, []);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const ref = useRef(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
const getBoundingRect = function getBoundingRect(ref_0) {
|
||||
if (ref_0.current) {
|
||||
return ref_0.current.getBoundingClientRect?.()?.width ?? 100;
|
||||
}
|
||||
return 100;
|
||||
};
|
||||
|
||||
setWidth(getBoundingRect(ref));
|
||||
};
|
||||
t1 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
return width;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 100
|
||||
@@ -0,0 +1,25 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
function getBoundingRect(ref) {
|
||||
if (ref.current) {
|
||||
return ref.current.getBoundingClientRect?.()?.width ?? 100;
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
|
||||
setWidth(getBoundingRect(ref));
|
||||
}, []);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useLayoutEffect} from 'react';
|
||||
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const {height} = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
|
||||
function Tooltip() {
|
||||
const $ = _c(2);
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
const { height } = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
};
|
||||
t1 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
useLayoutEffect(t0, t1);
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Cannot read properties of null (reading 'getBoundingClientRect')
|
||||
@@ -0,0 +1,19 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects
|
||||
import {useState, useRef, useLayoutEffect} from 'react';
|
||||
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const {height} = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
|
||||
return tooltipHeight;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_Activity as Activity,
|
||||
Activity,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useState,
|
||||
@@ -50,7 +50,8 @@ function Component() {
|
||||
<p>
|
||||
<img
|
||||
src="https://react.dev/_next/image?url=%2Fimages%2Fteam%2Fsebmarkbage.jpg&w=3840&q=75"
|
||||
width="300"
|
||||
width="400"
|
||||
height="248"
|
||||
/>
|
||||
</p>
|
||||
</ViewTransition>
|
||||
|
||||
@@ -56,10 +56,10 @@
|
||||
}
|
||||
|
||||
::view-transition-new(.enter-slide-right):only-child {
|
||||
animation: enter-slide-right ease-in 0.25s;
|
||||
animation: enter-slide-right ease-in 0.25s forwards;
|
||||
}
|
||||
::view-transition-old(.exit-slide-left):only-child {
|
||||
animation: exit-slide-left ease-in 0.25s;
|
||||
animation: exit-slide-left ease-in 0.25s forwards;
|
||||
}
|
||||
|
||||
:root:active-view-transition-type(navigation-back) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
clearErrors,
|
||||
createLogAssertion,
|
||||
} from './consoleMock';
|
||||
export {getDebugInfo} from './debugInfo';
|
||||
export {act, serverAct} from './internalAct';
|
||||
const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock');
|
||||
|
||||
|
||||
131
packages/internal-test-utils/debugInfo.js
Normal file
131
packages/internal-test-utils/debugInfo.js
Normal file
@@ -0,0 +1,131 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '../../');
|
||||
|
||||
type DebugInfoConfig = {
|
||||
ignoreProps?: boolean,
|
||||
ignoreRscStreamInfo?: boolean,
|
||||
useFixedTime?: boolean,
|
||||
useV8Stack?: boolean,
|
||||
};
|
||||
|
||||
function formatV8Stack(stack) {
|
||||
let v8StyleStack = '';
|
||||
if (stack) {
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const [name] = stack[i];
|
||||
if (v8StyleStack !== '') {
|
||||
v8StyleStack += '\n';
|
||||
}
|
||||
v8StyleStack += ' in ' + name + ' (at **)';
|
||||
}
|
||||
}
|
||||
return v8StyleStack;
|
||||
}
|
||||
|
||||
function normalizeStack(stack) {
|
||||
if (!stack) {
|
||||
return stack;
|
||||
}
|
||||
const copy = [];
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const [name, file, line, col, enclosingLine, enclosingCol] = stack[i];
|
||||
copy.push([
|
||||
name,
|
||||
file.replace(repoRoot, ''),
|
||||
line,
|
||||
col,
|
||||
enclosingLine,
|
||||
enclosingCol,
|
||||
]);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function normalizeIOInfo(config: DebugInfoConfig, ioInfo) {
|
||||
const {debugTask, debugStack, debugLocation, ...copy} = ioInfo;
|
||||
if (ioInfo.stack) {
|
||||
copy.stack = config.useV8Stack
|
||||
? formatV8Stack(ioInfo.stack)
|
||||
: normalizeStack(ioInfo.stack);
|
||||
}
|
||||
if (ioInfo.owner) {
|
||||
copy.owner = normalizeDebugInfo(config, ioInfo.owner);
|
||||
}
|
||||
if (typeof ioInfo.start === 'number' && config.useFixedTime) {
|
||||
copy.start = 0;
|
||||
}
|
||||
if (typeof ioInfo.end === 'number' && config.useFixedTime) {
|
||||
copy.end = 0;
|
||||
}
|
||||
const promise = ioInfo.value;
|
||||
if (promise) {
|
||||
promise.then(); // init
|
||||
if (promise.status === 'fulfilled') {
|
||||
if (ioInfo.name === 'RSC stream') {
|
||||
copy.byteSize = 0;
|
||||
copy.value = {
|
||||
value: 'stream',
|
||||
};
|
||||
} else {
|
||||
copy.value = {
|
||||
value: promise.value,
|
||||
};
|
||||
}
|
||||
} else if (promise.status === 'rejected') {
|
||||
copy.value = {
|
||||
reason: promise.reason,
|
||||
};
|
||||
} else {
|
||||
copy.value = {
|
||||
status: promise.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function normalizeDebugInfo(config: DebugInfoConfig, original) {
|
||||
const {debugTask, debugStack, debugLocation, ...debugInfo} = original;
|
||||
if (original.owner) {
|
||||
debugInfo.owner = normalizeDebugInfo(config, original.owner);
|
||||
}
|
||||
if (original.awaited) {
|
||||
debugInfo.awaited = normalizeIOInfo(config, original.awaited);
|
||||
}
|
||||
if (debugInfo.props && config.ignoreProps) {
|
||||
debugInfo.props = {};
|
||||
}
|
||||
if (Array.isArray(debugInfo.stack)) {
|
||||
debugInfo.stack = config.useV8Stack
|
||||
? formatV8Stack(debugInfo.stack)
|
||||
: normalizeStack(debugInfo.stack);
|
||||
return debugInfo;
|
||||
} else if (typeof debugInfo.time === 'number' && config.useFixedTime) {
|
||||
return {...debugInfo, time: 0};
|
||||
} else {
|
||||
return debugInfo;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDebugInfo(config: DebugInfoConfig, obj) {
|
||||
const debugInfo = obj._debugInfo;
|
||||
if (debugInfo) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
if (
|
||||
debugInfo[i].awaited &&
|
||||
debugInfo[i].awaited.name === 'RSC stream' &&
|
||||
config.ignoreRscStreamInfo
|
||||
) {
|
||||
// Ignore RSC stream I/O info.
|
||||
} else {
|
||||
copy.push(normalizeDebugInfo(config, debugInfo[i]));
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
return debugInfo;
|
||||
}
|
||||
10
packages/react-art/src/ReactFiberConfigART.js
vendored
10
packages/react-art/src/ReactFiberConfigART.js
vendored
@@ -609,13 +609,15 @@ export function preloadInstance(type, props) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function startSuspendingCommit() {}
|
||||
export function startSuspendingCommit() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function suspendInstance(instance, type, props) {}
|
||||
export function suspendInstance(state, instance, type, props) {}
|
||||
|
||||
export function suspendOnActiveViewTransition(container) {}
|
||||
export function suspendOnActiveViewTransition(state, container) {}
|
||||
|
||||
export function waitForCommitToBeReady() {
|
||||
export function waitForCommitToBeReady(timeoutOffset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
24
packages/react-client/src/ReactFlightClient.js
vendored
24
packages/react-client/src/ReactFlightClient.js
vendored
@@ -3181,11 +3181,27 @@ function resolveErrorDev(
|
||||
'An error occurred in the Server Components render but no message was provided',
|
||||
),
|
||||
);
|
||||
const rootTask = getRootTask(response, env);
|
||||
if (rootTask != null) {
|
||||
error = rootTask.run(callStack);
|
||||
|
||||
let ownerTask: null | ConsoleTask = null;
|
||||
if (errorInfo.owner != null) {
|
||||
const ownerRef = errorInfo.owner.slice(1);
|
||||
// TODO: This is not resilient to the owner loading later in an Error like a debug channel.
|
||||
// The whole error serialization should probably go through the regular model at least for DEV.
|
||||
const owner = getOutlinedModel(response, ownerRef, {}, '', createModel);
|
||||
if (owner !== null) {
|
||||
ownerTask = initializeFakeTask(response, owner);
|
||||
}
|
||||
}
|
||||
|
||||
if (ownerTask === null) {
|
||||
const rootTask = getRootTask(response, env);
|
||||
if (rootTask != null) {
|
||||
error = rootTask.run(callStack);
|
||||
} else {
|
||||
error = callStack();
|
||||
}
|
||||
} else {
|
||||
error = callStack();
|
||||
error = ownerTask.run(callStack);
|
||||
}
|
||||
|
||||
(error: any).name = name;
|
||||
|
||||
@@ -33,20 +33,6 @@ function normalizeCodeLocInfo(str) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatV8Stack(stack) {
|
||||
let v8StyleStack = '';
|
||||
if (stack) {
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const [name] = stack[i];
|
||||
if (v8StyleStack !== '') {
|
||||
v8StyleStack += '\n';
|
||||
}
|
||||
v8StyleStack += ' in ' + name + ' (at **)';
|
||||
}
|
||||
}
|
||||
return v8StyleStack;
|
||||
}
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '../../../../');
|
||||
function normalizeReactCodeLocInfo(str) {
|
||||
const repoRootForRegexp = repoRoot.replace(/\//g, '\\/');
|
||||
@@ -67,35 +53,6 @@ function getErrorForJestMatcher(error) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeComponentInfo(debugInfo) {
|
||||
if (Array.isArray(debugInfo.stack)) {
|
||||
const {debugTask, debugStack, debugLocation, ...copy} = debugInfo;
|
||||
copy.stack = formatV8Stack(debugInfo.stack);
|
||||
if (debugInfo.owner) {
|
||||
copy.owner = normalizeComponentInfo(debugInfo.owner);
|
||||
}
|
||||
return copy;
|
||||
} else {
|
||||
return debugInfo;
|
||||
}
|
||||
}
|
||||
|
||||
function getDebugInfo(obj) {
|
||||
const debugInfo = obj._debugInfo;
|
||||
if (debugInfo) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
if (debugInfo[i].awaited && debugInfo[i].awaited.name === 'RSC stream') {
|
||||
// Ignore RSC stream I/O info.
|
||||
} else {
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
return debugInfo;
|
||||
}
|
||||
|
||||
const finalizationRegistries = [];
|
||||
function FinalizationRegistryMock(callback) {
|
||||
this._heldValues = [];
|
||||
@@ -132,6 +89,7 @@ let NoErrorExpected;
|
||||
let Scheduler;
|
||||
let assertLog;
|
||||
let assertConsoleErrorDev;
|
||||
let getDebugInfo;
|
||||
|
||||
describe('ReactFlight', () => {
|
||||
beforeEach(() => {
|
||||
@@ -169,6 +127,11 @@ describe('ReactFlight', () => {
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
|
||||
|
||||
getDebugInfo = InternalTestUtils.getDebugInfo.bind(null, {
|
||||
useV8Stack: true,
|
||||
ignoreRscStreamInfo: true,
|
||||
});
|
||||
|
||||
ErrorBoundary = class extends React.Component {
|
||||
state = {hasError: false, error: null};
|
||||
static getDerivedStateFromError(error) {
|
||||
|
||||
@@ -18,50 +18,12 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') {
|
||||
global.FormData = require('undici').FormData;
|
||||
}
|
||||
|
||||
function formatV8Stack(stack) {
|
||||
let v8StyleStack = '';
|
||||
if (stack) {
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const [name] = stack[i];
|
||||
if (v8StyleStack !== '') {
|
||||
v8StyleStack += '\n';
|
||||
}
|
||||
v8StyleStack += ' in ' + name + ' (at **)';
|
||||
}
|
||||
}
|
||||
return v8StyleStack;
|
||||
}
|
||||
|
||||
function normalizeComponentInfo(debugInfo) {
|
||||
if (Array.isArray(debugInfo.stack)) {
|
||||
const {debugTask, debugStack, ...copy} = debugInfo;
|
||||
copy.stack = formatV8Stack(debugInfo.stack);
|
||||
if (debugInfo.owner) {
|
||||
copy.owner = normalizeComponentInfo(debugInfo.owner);
|
||||
}
|
||||
return copy;
|
||||
} else {
|
||||
return debugInfo;
|
||||
}
|
||||
}
|
||||
|
||||
function getDebugInfo(obj) {
|
||||
const debugInfo = obj._debugInfo;
|
||||
if (debugInfo) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
return debugInfo;
|
||||
}
|
||||
|
||||
let act;
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let ReactNoopFlightServer;
|
||||
let ReactNoopFlightClient;
|
||||
let getDebugInfo;
|
||||
|
||||
describe('ReactFlight', () => {
|
||||
beforeEach(() => {
|
||||
@@ -91,6 +53,11 @@ describe('ReactFlight', () => {
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
|
||||
act = require('internal-test-utils').act;
|
||||
|
||||
getDebugInfo = require('internal-test-utils').getDebugInfo.bind(null, {
|
||||
useV8Stack: true,
|
||||
ignoreRscStreamInfo: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -108,6 +108,7 @@ module.exports = {
|
||||
{
|
||||
loader: 'workerize-loader',
|
||||
options: {
|
||||
// Workers would have to be exposed on a public path in order to outline them.
|
||||
inline: true,
|
||||
name: '[name]',
|
||||
},
|
||||
|
||||
49
packages/react-devtools-extensions/build.js
vendored
49
packages/react-devtools-extensions/build.js
vendored
@@ -6,7 +6,7 @@ const archiver = require('archiver');
|
||||
const {execSync} = require('child_process');
|
||||
const {readFileSync, writeFileSync, createWriteStream} = require('fs');
|
||||
const {copy, ensureDir, move, remove, pathExistsSync} = require('fs-extra');
|
||||
const {join, resolve} = require('path');
|
||||
const {join, resolve, basename} = require('path');
|
||||
const {getGitCommit} = require('./utils');
|
||||
|
||||
// These files are copied along with Webpack-bundled files
|
||||
@@ -66,22 +66,31 @@ const build = async (tempPath, manifestPath, envExtension = {}) => {
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
execSync(
|
||||
`${webpackPath} --config webpack.backend.js --output-path ${binPath}`,
|
||||
{
|
||||
cwd: __dirname,
|
||||
env: mergedEnv,
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
|
||||
// Make temp dir
|
||||
await ensureDir(zipPath);
|
||||
|
||||
const copiedManifestPath = join(zipPath, 'manifest.json');
|
||||
|
||||
let webpackStatsFilePath = null;
|
||||
// Copy unbuilt source files to zip dir to be packaged:
|
||||
await copy(binPath, join(zipPath, 'build'));
|
||||
await copy(binPath, join(zipPath, 'build'), {
|
||||
filter: filePath => {
|
||||
if (basename(filePath).startsWith('webpack-stats.')) {
|
||||
webpackStatsFilePath = filePath;
|
||||
// The ZIP is the actual extension and doesn't need this metadata.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (webpackStatsFilePath !== null) {
|
||||
await copy(
|
||||
webpackStatsFilePath,
|
||||
join(tempPath, basename(webpackStatsFilePath)),
|
||||
);
|
||||
webpackStatsFilePath = join(tempPath, basename(webpackStatsFilePath));
|
||||
}
|
||||
await copy(manifestPath, copiedManifestPath);
|
||||
await Promise.all(
|
||||
STATIC_FILES.map(file => copy(join(__dirname, file), join(zipPath, file))),
|
||||
@@ -120,9 +129,11 @@ const build = async (tempPath, manifestPath, envExtension = {}) => {
|
||||
archive.finalize();
|
||||
zipStream.on('close', () => resolvePromise());
|
||||
});
|
||||
|
||||
return webpackStatsFilePath;
|
||||
};
|
||||
|
||||
const postProcess = async (tempPath, destinationPath) => {
|
||||
const postProcess = async (tempPath, destinationPath, webpackStatsFilePath) => {
|
||||
const unpackedSourcePath = join(tempPath, 'zip');
|
||||
const packedSourcePath = join(tempPath, 'ReactDevTools.zip');
|
||||
const packedDestPath = join(destinationPath, 'ReactDevTools.zip');
|
||||
@@ -130,6 +141,14 @@ const postProcess = async (tempPath, destinationPath) => {
|
||||
|
||||
await move(unpackedSourcePath, unpackedDestPath); // Copy built files to destination
|
||||
await move(packedSourcePath, packedDestPath); // Copy built files to destination
|
||||
if (webpackStatsFilePath !== null) {
|
||||
await move(
|
||||
webpackStatsFilePath,
|
||||
join(destinationPath, basename(webpackStatsFilePath)),
|
||||
);
|
||||
} else {
|
||||
console.log('No webpack-stats.json file was generated.');
|
||||
}
|
||||
await remove(tempPath); // Clean up temp directory and files
|
||||
};
|
||||
|
||||
@@ -158,10 +177,14 @@ const main = async buildId => {
|
||||
const tempPath = join(__dirname, 'build', buildId);
|
||||
await ensureLocalBuild();
|
||||
await preProcess(destinationPath, tempPath);
|
||||
await build(tempPath, manifestPath, envExtension);
|
||||
const webpackStatsFilePath = await build(
|
||||
tempPath,
|
||||
manifestPath,
|
||||
envExtension,
|
||||
);
|
||||
|
||||
const builtUnpackedPath = join(destinationPath, 'unpacked');
|
||||
await postProcess(tempPath, destinationPath);
|
||||
await postProcess(tempPath, destinationPath, webpackStatsFilePath);
|
||||
|
||||
return builtUnpackedPath;
|
||||
} catch (error) {
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"webpack": "^5.82.1",
|
||||
"webpack-cli": "^5.1.1",
|
||||
"webpack-dev-server": "^4.15.0",
|
||||
"webpack-stats-plugin": "^1.1.3",
|
||||
"workerize-loader": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -82,7 +82,7 @@ const fetchFromPage = async (url, resolve, reject) => {
|
||||
debugLog('[main] fetchFromPage()', url);
|
||||
|
||||
function onPortMessage({payload, source}) {
|
||||
if (source === 'react-devtools-background') {
|
||||
if (source === 'react-devtools-background' && payload?.url === url) {
|
||||
switch (payload?.type) {
|
||||
case 'fetch-file-with-cache-complete':
|
||||
chrome.runtime.onMessage.removeListener(onPortMessage);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
normalizeUrlIfValid,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {checkConditions} from 'react-devtools-shared/src/devtools/views/Editor/utils';
|
||||
import * as parseHookNames from 'react-devtools-shared/src/hooks/parseHookNames';
|
||||
|
||||
import {
|
||||
setBrowserSelectionFromReact,
|
||||
@@ -40,6 +41,12 @@ import getProfilingFlags from './getProfilingFlags';
|
||||
import debounce from './debounce';
|
||||
import './requestAnimationFramePolyfill';
|
||||
|
||||
const resolvedParseHookNames = Promise.resolve(parseHookNames);
|
||||
// DevTools assumes this is a dynamically imported module. Since we outline
|
||||
// workers in this bundle, we can sync require the module since it's just a thin
|
||||
// wrapper around calling the worker.
|
||||
const hookNamesModuleLoaderFunction = () => resolvedParseHookNames;
|
||||
|
||||
function createBridge() {
|
||||
bridge = new Bridge({
|
||||
listen(fn) {
|
||||
@@ -188,12 +195,6 @@ function createBridgeAndStore() {
|
||||
);
|
||||
};
|
||||
|
||||
// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
|
||||
const hookNamesModuleLoaderFunction = () =>
|
||||
import(
|
||||
/* webpackChunkName: 'parseHookNames' */ 'react-devtools-shared/src/hooks/parseHookNames'
|
||||
);
|
||||
|
||||
root = createRoot(document.createElement('div'));
|
||||
|
||||
render = (overrideTab = mostRecentOverrideTab) => {
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const {resolve, isAbsolute, relative} = require('path');
|
||||
const Webpack = require('webpack');
|
||||
|
||||
const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
|
||||
const SourceMapIgnoreListPlugin = require('react-devtools-shared/SourceMapIgnoreListPlugin');
|
||||
|
||||
const {GITHUB_URL, getVersionString} = require('./utils');
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
console.error('NODE_ENV not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const builtModulesDir = resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'build',
|
||||
'oss-experimental',
|
||||
);
|
||||
|
||||
const __DEV__ = NODE_ENV === 'development';
|
||||
|
||||
const DEVTOOLS_VERSION = getVersionString(process.env.DEVTOOLS_VERSION);
|
||||
|
||||
const IS_CHROME = process.env.IS_CHROME === 'true';
|
||||
const IS_FIREFOX = process.env.IS_FIREFOX === 'true';
|
||||
const IS_EDGE = process.env.IS_EDGE === 'true';
|
||||
|
||||
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
|
||||
|
||||
module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: false,
|
||||
entry: {
|
||||
backend: './src/backend.js',
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/build',
|
||||
filename: 'react_devtools_backend_compact.js',
|
||||
},
|
||||
node: {
|
||||
global: false,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
react: resolve(builtModulesDir, 'react'),
|
||||
'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
|
||||
'react-devtools-feature-flags': resolveFeatureFlags(featureFlagTarget),
|
||||
'react-dom': resolve(builtModulesDir, 'react-dom'),
|
||||
'react-is': resolve(builtModulesDir, 'react-is'),
|
||||
scheduler: resolve(builtModulesDir, 'scheduler'),
|
||||
},
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
plugins: [
|
||||
new Webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
}),
|
||||
new Webpack.DefinePlugin({
|
||||
__DEV__: true,
|
||||
__PROFILE__: false,
|
||||
__DEV____DEV__: true,
|
||||
// By importing `shared/` we may import ReactFeatureFlags
|
||||
__EXPERIMENTAL__: true,
|
||||
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
|
||||
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
|
||||
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
|
||||
'process.env.IS_CHROME': IS_CHROME,
|
||||
'process.env.IS_FIREFOX': IS_FIREFOX,
|
||||
'process.env.IS_EDGE': IS_EDGE,
|
||||
__IS_CHROME__: IS_CHROME,
|
||||
__IS_FIREFOX__: IS_FIREFOX,
|
||||
__IS_EDGE__: IS_EDGE,
|
||||
__IS_NATIVE__: false,
|
||||
__IS_INTERNAL_MCP_BUILD__: false,
|
||||
}),
|
||||
new Webpack.SourceMapDevToolPlugin({
|
||||
filename: '[file].map',
|
||||
noSources: !__DEV__,
|
||||
// https://github.com/webpack/webpack/issues/3603#issuecomment-1743147144
|
||||
moduleFilenameTemplate(info) {
|
||||
const {absoluteResourcePath, namespace, resourcePath} = info;
|
||||
|
||||
if (isAbsolute(absoluteResourcePath)) {
|
||||
return relative(__dirname + '/build', absoluteResourcePath);
|
||||
}
|
||||
|
||||
// Mimic Webpack's default behavior:
|
||||
return `webpack://${namespace}/${resourcePath}`;
|
||||
},
|
||||
}),
|
||||
new SourceMapIgnoreListPlugin({
|
||||
shouldIgnoreSource: () => !__DEV__,
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
configFile: resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'react-devtools-shared',
|
||||
'babel.config.js',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -6,6 +6,7 @@ const TerserPlugin = require('terser-webpack-plugin');
|
||||
const {GITHUB_URL, getVersionString} = require('./utils');
|
||||
const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
|
||||
const SourceMapIgnoreListPlugin = require('react-devtools-shared/SourceMapIgnoreListPlugin');
|
||||
const {StatsWriterPlugin} = require('webpack-stats-plugin');
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
@@ -37,6 +38,21 @@ const IS_INTERNAL_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true';
|
||||
|
||||
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
|
||||
|
||||
let statsFileName = `webpack-stats.${featureFlagTarget}.${__DEV__ ? 'development' : 'production'}`;
|
||||
if (IS_CHROME) {
|
||||
statsFileName += `.chrome`;
|
||||
}
|
||||
if (IS_FIREFOX) {
|
||||
statsFileName += `.firefox`;
|
||||
}
|
||||
if (IS_EDGE) {
|
||||
statsFileName += `.edge`;
|
||||
}
|
||||
if (IS_INTERNAL_MCP_BUILD) {
|
||||
statsFileName += `.mcp`;
|
||||
}
|
||||
statsFileName += '.json';
|
||||
|
||||
const babelOptions = {
|
||||
configFile: resolve(
|
||||
__dirname,
|
||||
@@ -50,6 +66,7 @@ module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: false,
|
||||
entry: {
|
||||
backend: './src/backend.js',
|
||||
background: './src/background/index.js',
|
||||
backendManager: './src/contentScripts/backendManager.js',
|
||||
fileFetcher: './src/contentScripts/fileFetcher.js',
|
||||
@@ -63,7 +80,14 @@ module.exports = {
|
||||
output: {
|
||||
path: __dirname + '/build',
|
||||
publicPath: '/build/',
|
||||
filename: '[name].js',
|
||||
filename: chunkData => {
|
||||
switch (chunkData.chunk.name) {
|
||||
case 'backend':
|
||||
return 'react_devtools_backend_compact.js';
|
||||
default:
|
||||
return '[name].js';
|
||||
}
|
||||
},
|
||||
chunkFilename: '[name].chunk.js',
|
||||
},
|
||||
node: {
|
||||
@@ -103,7 +127,6 @@ module.exports = {
|
||||
plugins: [
|
||||
new Webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
new Webpack.DefinePlugin({
|
||||
__DEV__,
|
||||
@@ -126,7 +149,7 @@ module.exports = {
|
||||
}),
|
||||
new Webpack.SourceMapDevToolPlugin({
|
||||
filename: '[file].map',
|
||||
include: 'installHook.js',
|
||||
include: ['installHook.js', 'react_devtools_backend_compact.js'],
|
||||
noSources: !__DEV__,
|
||||
// https://github.com/webpack/webpack/issues/3603#issuecomment-1743147144
|
||||
moduleFilenameTemplate(info) {
|
||||
@@ -148,6 +171,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
const contentScriptNamesToIgnoreList = [
|
||||
'react_devtools_backend_compact',
|
||||
// This is where we override console
|
||||
'installHook',
|
||||
];
|
||||
@@ -213,6 +237,10 @@ module.exports = {
|
||||
);
|
||||
},
|
||||
},
|
||||
new StatsWriterPlugin({
|
||||
stats: 'verbose',
|
||||
filename: statsFileName,
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
defaultRules: [
|
||||
@@ -233,7 +261,7 @@ module.exports = {
|
||||
{
|
||||
loader: 'workerize-loader',
|
||||
options: {
|
||||
inline: true,
|
||||
inline: false,
|
||||
name: '[name]',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -74,7 +74,6 @@ module.exports = {
|
||||
new MiniCssExtractPlugin(),
|
||||
new Webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
new Webpack.DefinePlugin({
|
||||
__DEV__,
|
||||
@@ -102,6 +101,7 @@ module.exports = {
|
||||
{
|
||||
loader: 'workerize-loader',
|
||||
options: {
|
||||
// Workers would have to be exposed on a public path in order to outline them.
|
||||
inline: true,
|
||||
name: '[name]',
|
||||
},
|
||||
|
||||
@@ -93,7 +93,9 @@ test.describe('Components', () => {
|
||||
|
||||
const name = isEditable.name
|
||||
? existingNameElements[0].value
|
||||
: existingNameElements[0].innerText;
|
||||
: existingNameElements[0].innerText
|
||||
// remove trailing colon
|
||||
.slice(0, -1);
|
||||
const value = isEditable.value
|
||||
? existingValueElements[0].value
|
||||
: existingValueElements[0].innerText;
|
||||
|
||||
@@ -65,7 +65,6 @@ module.exports = {
|
||||
plugins: [
|
||||
new Webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
new Webpack.DefinePlugin({
|
||||
__DEV__,
|
||||
@@ -94,6 +93,7 @@ module.exports = {
|
||||
{
|
||||
loader: 'workerize-loader',
|
||||
options: {
|
||||
// Workers would have to be exposed on a public path in order to outline them.
|
||||
inline: true,
|
||||
name: '[name]',
|
||||
},
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
"react-dom-15": "npm:react-dom@^15"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.12.5",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@reach/menu-button": "^0.16.1",
|
||||
"@reach/tooltip": "^0.16.0",
|
||||
"clipboard-js": "^0.3.6",
|
||||
|
||||
@@ -12,8 +12,8 @@ export function test(maybeStore) {
|
||||
}
|
||||
|
||||
// print() is part of Jest's serializer API
|
||||
export function print(store, serialize, indent) {
|
||||
return printStore(store);
|
||||
export function print(store, serialize, indent, includeSuspense = true) {
|
||||
return printStore(store, false, null, includeSuspense);
|
||||
}
|
||||
|
||||
// Used for Jest snapshot testing.
|
||||
|
||||
@@ -724,34 +724,69 @@ describe('ProfilingCache', () => {
|
||||
const rootID = store.roots[0];
|
||||
const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
|
||||
expect(commitData).toHaveLength(2);
|
||||
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 15,
|
||||
2 => 15,
|
||||
3 => 5,
|
||||
4 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[0].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 0,
|
||||
2 => 10,
|
||||
3 => 3,
|
||||
4 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
5 => 3,
|
||||
3 => 3,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
5 => 3,
|
||||
3 => 0,
|
||||
}
|
||||
`);
|
||||
|
||||
const isLegacySuspense = React.version.startsWith('17');
|
||||
if (isLegacySuspense) {
|
||||
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 15,
|
||||
2 => 15,
|
||||
3 => 5,
|
||||
4 => 3,
|
||||
5 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[0].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 0,
|
||||
2 => 10,
|
||||
3 => 3,
|
||||
4 => 3,
|
||||
5 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
6 => 3,
|
||||
3 => 3,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
6 => 3,
|
||||
3 => 0,
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 15,
|
||||
2 => 15,
|
||||
3 => 5,
|
||||
4 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[0].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 0,
|
||||
2 => 10,
|
||||
3 => 3,
|
||||
4 => 2,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
5 => 3,
|
||||
3 => 3,
|
||||
}
|
||||
`);
|
||||
expect(commitData[1].fiberSelfDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
5 => 3,
|
||||
3 => 0,
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
// @reactVersion >= 16.9
|
||||
@@ -866,6 +901,7 @@ describe('ProfilingCache', () => {
|
||||
"hocDisplayNames": null,
|
||||
"id": 1,
|
||||
"key": null,
|
||||
"stack": null,
|
||||
"type": 11,
|
||||
},
|
||||
],
|
||||
@@ -908,6 +944,7 @@ describe('ProfilingCache', () => {
|
||||
"hocDisplayNames": null,
|
||||
"id": 1,
|
||||
"key": null,
|
||||
"stack": null,
|
||||
"type": 11,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -15,10 +15,12 @@ import {
|
||||
} from './utils';
|
||||
|
||||
describe('commit tree', () => {
|
||||
let React;
|
||||
let React = require('react');
|
||||
let Scheduler;
|
||||
let store: Store;
|
||||
let utils;
|
||||
const isLegacySuspense =
|
||||
React.version.startsWith('16') || React.version.startsWith('17');
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
@@ -184,17 +186,32 @@ describe('commit tree', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
await Promise.resolve();
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
`);
|
||||
if (isLegacySuspense) {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<Lazy>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
} else {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
}
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<LazyInnerComponent>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
utils.act(() => legacyRender(<App renderChildren={false} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
@@ -214,7 +231,13 @@ describe('commit tree', () => {
|
||||
);
|
||||
}
|
||||
|
||||
expect(commitTrees[0].nodes.size).toBe(3); // <Root> + <App> + <Suspense>
|
||||
expect(commitTrees[0].nodes.size).toBe(
|
||||
isLegacySuspense
|
||||
? // <Root> + <App> + <Suspense> + <Lazy>
|
||||
4
|
||||
: // <Root> + <App> + <Suspense>
|
||||
3,
|
||||
);
|
||||
expect(commitTrees[1].nodes.size).toBe(4); // <Root> + <App> + <Suspense> + <LazyInnerComponent>
|
||||
expect(commitTrees[2].nodes.size).toBe(2); // <Root> + <App>
|
||||
});
|
||||
@@ -268,11 +291,24 @@ describe('commit tree', () => {
|
||||
it('should support Lazy components that are unmounted before resolving (legacy render)', async () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
`);
|
||||
if (isLegacySuspense) {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<Lazy>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
} else {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
}
|
||||
utils.act(() => legacyRender(<App renderChildren={false} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
@@ -291,7 +327,13 @@ describe('commit tree', () => {
|
||||
);
|
||||
}
|
||||
|
||||
expect(commitTrees[0].nodes.size).toBe(3); // <Root> + <App> + <Suspense>
|
||||
expect(commitTrees[0].nodes.size).toBe(
|
||||
isLegacySuspense
|
||||
? // <Root> + <App> + <Suspense> + <Lazy>
|
||||
4
|
||||
: // <Root> + <App> + <Suspense>
|
||||
3,
|
||||
);
|
||||
expect(commitTrees[1].nodes.size).toBe(2); // <Root> + <App>
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,35 @@ describe('Store', () => {
|
||||
let store;
|
||||
let withErrorsOrWarningsIgnored;
|
||||
|
||||
function readValue(promise) {
|
||||
if (typeof React.use === 'function') {
|
||||
return React.use(promise);
|
||||
}
|
||||
|
||||
// Support for React < 19.0
|
||||
switch (promise.status) {
|
||||
case 'fulfilled':
|
||||
return promise.value;
|
||||
case 'rejected':
|
||||
throw promise.reason;
|
||||
case 'pending':
|
||||
throw promise;
|
||||
default:
|
||||
promise.status = 'pending';
|
||||
promise.then(
|
||||
value => {
|
||||
promise.status = 'fulfilled';
|
||||
promise.value = value;
|
||||
},
|
||||
reason => {
|
||||
promise.status = 'rejected';
|
||||
promise.reason = reason;
|
||||
},
|
||||
);
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
// JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
|
||||
Element.prototype.getClientRects = function (this: Element) {
|
||||
@@ -107,11 +136,7 @@ describe('Store', () => {
|
||||
let Dynamic = null;
|
||||
const Owner = () => {
|
||||
Dynamic = <Child />;
|
||||
if (React.use) {
|
||||
React.use(promise);
|
||||
} else {
|
||||
throw promise;
|
||||
}
|
||||
readValue(promise);
|
||||
};
|
||||
const Parent = () => {
|
||||
return Dynamic;
|
||||
@@ -462,12 +487,9 @@ describe('Store', () => {
|
||||
// @reactVersion >= 18.0
|
||||
it('should display Suspense nodes properly in various states', async () => {
|
||||
const Loading = () => <div>Loading...</div>;
|
||||
const never = new Promise(() => {});
|
||||
const SuspendingComponent = () => {
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
readValue(never);
|
||||
};
|
||||
const Component = () => {
|
||||
return <div>Hello</div>;
|
||||
@@ -514,12 +536,9 @@ describe('Store', () => {
|
||||
it('should support nested Suspense nodes', async () => {
|
||||
const Component = () => null;
|
||||
const Loading = () => <div>Loading...</div>;
|
||||
const never = new Promise(() => {});
|
||||
const Never = () => {
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
readValue(never);
|
||||
};
|
||||
|
||||
const Wrapper = ({
|
||||
@@ -1019,12 +1038,9 @@ describe('Store', () => {
|
||||
|
||||
it('should display a partially rendered SuspenseList', async () => {
|
||||
const Loading = () => <div>Loading...</div>;
|
||||
const never = new Promise(() => {});
|
||||
const SuspendingComponent = () => {
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
readValue(never);
|
||||
};
|
||||
const Component = () => {
|
||||
return <div>Hello</div>;
|
||||
@@ -1379,12 +1395,9 @@ describe('Store', () => {
|
||||
// @reactVersion >= 18.0
|
||||
it('should display Suspense nodes properly in various states', async () => {
|
||||
const Loading = () => <div>Loading...</div>;
|
||||
const never = new Promise(() => {});
|
||||
const SuspendingComponent = () => {
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
readValue(never);
|
||||
};
|
||||
const Component = () => {
|
||||
return <div>Hello</div>;
|
||||
@@ -2081,6 +2094,8 @@ describe('Store', () => {
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
|
||||
// Render again to unmount it before it finishes loading
|
||||
@@ -2489,7 +2504,7 @@ describe('Store', () => {
|
||||
withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
||||
await act(() => render(<React.Fragment />));
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(`[root]`);
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
expect(store.componentWithErrorCount).toBe(0);
|
||||
expect(store.componentWithWarningCount).toBe(0);
|
||||
});
|
||||
@@ -2826,7 +2841,7 @@ describe('Store', () => {
|
||||
|
||||
function Component({children, promise}) {
|
||||
if (promise) {
|
||||
React.use(promise);
|
||||
readValue(promise);
|
||||
}
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
@@ -2901,7 +2916,7 @@ describe('Store', () => {
|
||||
|
||||
function Component({children, promise}) {
|
||||
if (promise) {
|
||||
React.use(promise);
|
||||
readValue(promise);
|
||||
}
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
@@ -3079,10 +3094,17 @@ describe('Store', () => {
|
||||
<Suspense name="head-fallback" rects={[{x:1,y:2,width:10,height:1}]}>
|
||||
<Suspense name="main" rects={[{x:1,y:2,width:4,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => render(null));
|
||||
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
});
|
||||
|
||||
it('should handle an empty root', async () => {
|
||||
await actAsync(() => render(null));
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
|
||||
await actAsync(() => render(<span />));
|
||||
expect(store).toMatchInlineSnapshot(`[root]`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('Store component filters', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 16.0
|
||||
// @reactVersion >= 16.6
|
||||
it('should filter Suspense', async () => {
|
||||
const Suspense = React.Suspense;
|
||||
await actAsync(async () =>
|
||||
@@ -199,7 +199,7 @@ describe('Store component filters', () => {
|
||||
});
|
||||
|
||||
it('should filter Activity', async () => {
|
||||
const Activity = React.unstable_Activity;
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
|
||||
if (Activity != null) {
|
||||
await actAsync(async () =>
|
||||
|
||||
@@ -16,6 +16,35 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
let store;
|
||||
let print;
|
||||
|
||||
function readValue(promise) {
|
||||
if (typeof React.use === 'function') {
|
||||
return React.use(promise);
|
||||
}
|
||||
|
||||
// Support for React < 19.0
|
||||
switch (promise.status) {
|
||||
case 'fulfilled':
|
||||
return promise.value;
|
||||
case 'rejected':
|
||||
throw promise.reason;
|
||||
case 'pending':
|
||||
throw promise;
|
||||
default:
|
||||
promise.status = 'pending';
|
||||
promise.then(
|
||||
value => {
|
||||
promise.status = 'fulfilled';
|
||||
promise.value = value;
|
||||
},
|
||||
reason => {
|
||||
promise.status = 'rejected';
|
||||
promise.reason = reason;
|
||||
},
|
||||
);
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
@@ -415,118 +444,116 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
a,
|
||||
];
|
||||
|
||||
// Excluding Suspense tree here due to different measurement semantics for fallbacks
|
||||
const stepsSnapshot = [
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<B key="b">
|
||||
<A key="a">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
<Suspense>
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<B key="b">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
"[root]
|
||||
▾ <Root>
|
||||
<X>
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
<Y>"
|
||||
`,
|
||||
];
|
||||
|
||||
const never = new Promise(() => {});
|
||||
const Never = () => {
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
readValue(never);
|
||||
};
|
||||
|
||||
const Root = ({children}) => {
|
||||
@@ -549,8 +576,10 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// We snapshot each step once so it doesn't regress.
|
||||
expect(store).toMatchInlineSnapshot(stepsSnapshot[i]);
|
||||
snapshots.push(print(store));
|
||||
expect(print(store, undefined, undefined, false)).toMatchInlineSnapshot(
|
||||
stepsSnapshot[i],
|
||||
);
|
||||
snapshots.push(print(store, undefined, undefined, false));
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
}
|
||||
@@ -572,7 +601,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
}
|
||||
@@ -592,7 +621,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -604,7 +633,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -615,7 +644,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -641,7 +670,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -657,7 +686,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -672,7 +701,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -694,7 +723,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -710,7 +739,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -721,7 +750,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -747,7 +776,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -759,7 +788,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -774,7 +803,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -803,7 +832,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
const suspenseID = store.getElementIDAtIndex(2);
|
||||
|
||||
// Force fallback.
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
act(() => {
|
||||
bridge.send('overrideSuspense', {
|
||||
id: suspenseID,
|
||||
@@ -811,7 +840,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
forceFallback: true,
|
||||
});
|
||||
});
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
|
||||
// Stop forcing fallback.
|
||||
act(() => {
|
||||
@@ -821,7 +850,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
forceFallback: false,
|
||||
});
|
||||
});
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
|
||||
// Trigger actual fallback.
|
||||
act(() =>
|
||||
@@ -837,7 +866,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
|
||||
// Force fallback while we're in fallback mode.
|
||||
act(() => {
|
||||
@@ -848,7 +877,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
});
|
||||
});
|
||||
// Keep seeing fallback content.
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
|
||||
// Switch to primary mode.
|
||||
act(() =>
|
||||
@@ -861,7 +890,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Fallback is still forced though.
|
||||
expect(print(store)).toEqual(snapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
|
||||
|
||||
// Stop forcing fallback. This reverts to primary content.
|
||||
act(() => {
|
||||
@@ -872,7 +901,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
});
|
||||
});
|
||||
// Now we see primary content.
|
||||
expect(print(store)).toEqual(snapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
|
||||
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
@@ -921,6 +950,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -931,6 +962,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -943,6 +976,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<C key="c">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -955,6 +990,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -966,6 +1003,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -977,6 +1016,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -988,6 +1029,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -999,6 +1042,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<B key="b">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1009,6 +1054,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1018,6 +1065,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <MaybeSuspend>
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1028,6 +1077,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<B key="b">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1038,6 +1089,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<Z>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1049,6 +1102,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1057,6 +1112,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1067,6 +1124,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<B key="b">
|
||||
<C key="c">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1077,6 +1136,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<B key="b">
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1086,6 +1147,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1095,6 +1158,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1104,6 +1169,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<C key="c">
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1113,6 +1180,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<A key="a">
|
||||
<B key="b">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1121,6 +1190,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1128,6 +1199,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
<X>
|
||||
<Suspense>
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1136,6 +1209,8 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<B key="b">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
`
|
||||
[root]
|
||||
@@ -1144,15 +1219,14 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
▾ <Suspense>
|
||||
<A key="a">
|
||||
<Y>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`,
|
||||
];
|
||||
|
||||
const never = new Promise(() => {});
|
||||
const Never = () => {
|
||||
if (React.use) {
|
||||
React.use(new Promise(() => {}));
|
||||
} else {
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
readValue(never);
|
||||
};
|
||||
|
||||
const MaybeSuspend = ({children, suspend}) => {
|
||||
@@ -1224,7 +1298,7 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
);
|
||||
// We snapshot each step once so it doesn't regress.
|
||||
expect(store).toMatchInlineSnapshot(stepsSnapshotTwo[i]);
|
||||
fallbackSnapshots.push(print(store));
|
||||
fallbackSnapshots.push(print(store, undefined, undefined, false));
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
}
|
||||
@@ -1302,7 +1376,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[i],
|
||||
);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -1321,7 +1397,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -1339,7 +1417,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[i],
|
||||
);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -1377,7 +1457,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Verify the successful transition to steps[j].
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
// Check that we can transition back again.
|
||||
act(() =>
|
||||
render(
|
||||
@@ -1414,7 +1496,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[i],
|
||||
);
|
||||
// Re-render with steps[j].
|
||||
act(() =>
|
||||
render(
|
||||
@@ -1441,7 +1525,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[i]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[i],
|
||||
);
|
||||
// Clean up after every iteration.
|
||||
act(() => unmount());
|
||||
expect(print(store)).toBe('');
|
||||
@@ -1480,7 +1566,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
forceFallback: true,
|
||||
});
|
||||
});
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
|
||||
// Stop forcing fallback.
|
||||
act(() => {
|
||||
@@ -1504,7 +1592,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
|
||||
// Force fallback while we're in fallback mode.
|
||||
act(() => {
|
||||
@@ -1515,7 +1605,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
});
|
||||
});
|
||||
// Keep seeing fallback content.
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
|
||||
// Switch to primary mode.
|
||||
act(() =>
|
||||
@@ -1530,7 +1622,9 @@ describe('StoreStress (Legacy Mode)', () => {
|
||||
),
|
||||
);
|
||||
// Fallback is still forced though.
|
||||
expect(print(store)).toEqual(fallbackSnapshots[j]);
|
||||
expect(print(store, undefined, undefined, false)).toEqual(
|
||||
fallbackSnapshots[j],
|
||||
);
|
||||
|
||||
// Stop forcing fallback. This reverts to primary content.
|
||||
act(() => {
|
||||
|
||||
@@ -88,6 +88,7 @@ import {
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
SUSPENSE_TREE_OPERATION_RESIZE,
|
||||
SUSPENSE_TREE_OPERATION_SUSPENDERS,
|
||||
UNKNOWN_SUSPENDERS_NONE,
|
||||
UNKNOWN_SUSPENDERS_REASON_PRODUCTION,
|
||||
UNKNOWN_SUSPENDERS_REASON_OLD_VERSION,
|
||||
@@ -1515,7 +1516,7 @@ export function attach(
|
||||
currentRoot = rootInstance;
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
flushPendingEvents(root);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
@@ -1540,7 +1541,7 @@ export function attach(
|
||||
currentRoot = newRoot;
|
||||
setRootPseudoKey(currentRoot.id, root.current);
|
||||
mountFiberRecursively(root.current, false);
|
||||
flushPendingEvents(root);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
@@ -2016,6 +2017,7 @@ export function attach(
|
||||
const pendingOperations: OperationsArray = [];
|
||||
const pendingRealUnmountedIDs: Array<FiberInstance['id']> = [];
|
||||
const pendingRealUnmountedSuspenseIDs: Array<FiberInstance['id']> = [];
|
||||
const pendingSuspenderChanges: Set<FiberInstance['id']> = new Set();
|
||||
let pendingOperationsQueue: Array<OperationsArray> | null = [];
|
||||
const pendingStringTable: Map<string, StringTableEntry> = new Map();
|
||||
let pendingStringTableLength: number = 0;
|
||||
@@ -2047,6 +2049,7 @@ export function attach(
|
||||
pendingOperations.length === 0 &&
|
||||
pendingRealUnmountedIDs.length === 0 &&
|
||||
pendingRealUnmountedSuspenseIDs.length === 0 &&
|
||||
pendingSuspenderChanges.size === 0 &&
|
||||
pendingUnmountedRootID === null
|
||||
);
|
||||
}
|
||||
@@ -2096,7 +2099,7 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function flushPendingEvents(root: Object): void {
|
||||
function flushPendingEvents(): void {
|
||||
if (shouldBailoutWithPendingOperations()) {
|
||||
// If we aren't profiling, we can just bail out here.
|
||||
// No use sending an empty update over the bridge.
|
||||
@@ -2113,6 +2116,7 @@ export function attach(
|
||||
pendingRealUnmountedIDs.length +
|
||||
(pendingUnmountedRootID === null ? 0 : 1);
|
||||
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
|
||||
const numSuspenderChanges = pendingSuspenderChanges.size;
|
||||
|
||||
const operations = new Array<number>(
|
||||
// Identify which renderer this update is coming from.
|
||||
@@ -2128,7 +2132,10 @@ export function attach(
|
||||
// [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
|
||||
(numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
|
||||
// Regular operations
|
||||
pendingOperations.length,
|
||||
pendingOperations.length +
|
||||
// All suspender changes are batched in a single message.
|
||||
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders]]
|
||||
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 2 : 0),
|
||||
);
|
||||
|
||||
// Identify which renderer this update is coming from.
|
||||
@@ -2191,12 +2198,31 @@ export function attach(
|
||||
i++;
|
||||
}
|
||||
}
|
||||
// Fill in the rest of the operations.
|
||||
|
||||
// Fill in pending operations.
|
||||
for (let j = 0; j < pendingOperations.length; j++) {
|
||||
operations[i + j] = pendingOperations[j];
|
||||
}
|
||||
i += pendingOperations.length;
|
||||
|
||||
// Suspender changes might affect newly mounted nodes that we already recorded
|
||||
// in pending operations.
|
||||
if (numSuspenderChanges > 0) {
|
||||
operations[i++] = SUSPENSE_TREE_OPERATION_SUSPENDERS;
|
||||
operations[i++] = numSuspenderChanges;
|
||||
pendingSuspenderChanges.forEach(fiberIdWithChanges => {
|
||||
const suspense = idToSuspenseNodeMap.get(fiberIdWithChanges);
|
||||
if (suspense === undefined) {
|
||||
// Probably forgot to cleanup pendingSuspenderChanges when this node was removed.
|
||||
throw new Error(
|
||||
`Could not send suspender changes for "${fiberIdWithChanges}" since the Fiber no longer exists.`,
|
||||
);
|
||||
}
|
||||
operations[i++] = fiberIdWithChanges;
|
||||
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Let the frontend know about tree operations.
|
||||
flushOrQueueOperations(operations);
|
||||
|
||||
@@ -2204,6 +2230,7 @@ export function attach(
|
||||
pendingOperations.length = 0;
|
||||
pendingRealUnmountedIDs.length = 0;
|
||||
pendingRealUnmountedSuspenseIDs.length = 0;
|
||||
pendingSuspenderChanges.clear();
|
||||
pendingUnmountedRootID = null;
|
||||
pendingStringTable.clear();
|
||||
pendingStringTableLength = 0;
|
||||
@@ -2688,6 +2715,19 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function recordSuspenseSuspenders(suspenseNode: SuspenseNode): void {
|
||||
if (__DEBUG__) {
|
||||
console.log('recordSuspenseSuspenders()', suspenseNode);
|
||||
}
|
||||
const fiberInstance = suspenseNode.instance;
|
||||
if (fiberInstance.kind !== FIBER_INSTANCE) {
|
||||
// TODO: Suspender updates of filtered Suspense nodes are currently dropped.
|
||||
return;
|
||||
}
|
||||
|
||||
pendingSuspenderChanges.add(fiberInstance.id);
|
||||
}
|
||||
|
||||
function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void {
|
||||
if (__DEBUG__) {
|
||||
console.log(
|
||||
@@ -2709,6 +2749,7 @@ export function attach(
|
||||
// and later arrange them in the correct order.
|
||||
pendingRealUnmountedSuspenseIDs.push(id);
|
||||
|
||||
pendingSuspenderChanges.delete(id);
|
||||
idToSuspenseNodeMap.delete(id);
|
||||
}
|
||||
|
||||
@@ -2779,6 +2820,7 @@ export function attach(
|
||||
) {
|
||||
// This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
|
||||
parentSuspenseNode.hasUniqueSuspenders = true;
|
||||
recordSuspenseSuspenders(parentSuspenseNode);
|
||||
}
|
||||
}
|
||||
// We have observed at least one known reason this might have been suspended.
|
||||
@@ -2820,6 +2862,9 @@ export function attach(
|
||||
// We have found a child boundary that depended on the unblocked I/O.
|
||||
// It can now be marked as having unique suspenders. We can skip its children
|
||||
// since they'll still be blocked by this one.
|
||||
if (!node.hasUniqueSuspenders) {
|
||||
recordSuspenseSuspenders(node);
|
||||
}
|
||||
node.hasUniqueSuspenders = true;
|
||||
node.hasUnknownSuspenders = false;
|
||||
} else if (node.firstChild !== null) {
|
||||
@@ -2857,16 +2902,32 @@ export function attach(
|
||||
// Let's remove it from the parent SuspenseNode.
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
const suspendedBySet = parentSuspenseNode.suspendedBy.get(ioInfo);
|
||||
|
||||
if (
|
||||
suspendedBySet === undefined ||
|
||||
!suspendedBySet.delete(instance)
|
||||
) {
|
||||
throw new Error(
|
||||
'We are cleaning up async info that was not on the parent Suspense boundary. ' +
|
||||
'This is a bug in React.',
|
||||
);
|
||||
// A boundary can await the same IO multiple times.
|
||||
// We still want to error if we're trying to remove IO that isn't present on
|
||||
// this boundary so we need to check if we've already removed it.
|
||||
// We're assuming previousSuspendedBy is a small array so this should be faster
|
||||
// than allocating and maintaining a Set.
|
||||
let alreadyRemovedIO = false;
|
||||
for (let j = 0; j < i; j++) {
|
||||
const removedIOInfo = previousSuspendedBy[j].awaited;
|
||||
if (removedIOInfo === ioInfo) {
|
||||
alreadyRemovedIO = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alreadyRemovedIO) {
|
||||
throw new Error(
|
||||
'We are cleaning up async info that was not on the parent Suspense boundary. ' +
|
||||
'This is a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (suspendedBySet.size === 0) {
|
||||
if (suspendedBySet !== undefined && suspendedBySet.size === 0) {
|
||||
parentSuspenseNode.suspendedBy.delete(asyncInfo.awaited);
|
||||
}
|
||||
if (
|
||||
@@ -3025,6 +3086,24 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function unmountSuspenseChildrenRecursively(
|
||||
contentInstance: DevToolsInstance,
|
||||
stashedSuspenseParent: null | SuspenseNode,
|
||||
stashedSuspensePrevious: null | SuspenseNode,
|
||||
stashedSuspenseRemaining: null | SuspenseNode,
|
||||
): void {
|
||||
// First unmount only the Offscreen boundary. I.e. the main content.
|
||||
unmountInstanceRecursively(contentInstance);
|
||||
|
||||
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
|
||||
// unmount the fallback, unmounting anything in the context of the parent SuspenseNode.
|
||||
// Since the fallback conceptually blocks the parent.
|
||||
reconcilingParentSuspenseNode = stashedSuspenseParent;
|
||||
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
|
||||
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
|
||||
unmountRemainingChildren();
|
||||
}
|
||||
|
||||
function isChildOf(
|
||||
parentInstance: DevToolsInstance,
|
||||
childInstance: DevToolsInstance,
|
||||
@@ -3522,6 +3601,9 @@ export function attach(
|
||||
// Unfortunately if we don't have any DEV time debug info or debug thenables then
|
||||
// we have no meta data to show. However, we still mark this Suspense boundary as
|
||||
// participating in the loading sequence since apparently it can suspend.
|
||||
if (!suspenseNode.hasUniqueSuspenders) {
|
||||
recordSuspenseSuspenders(suspenseNode);
|
||||
}
|
||||
suspenseNode.hasUniqueSuspenders = true;
|
||||
// We have not seen any reason yet for why this suspense node might have been
|
||||
// suspended but it clearly has been at some point. If we later discover a reason
|
||||
@@ -3967,6 +4049,7 @@ export function attach(
|
||||
debug('unmountInstanceRecursively()', instance, reconcilingParent);
|
||||
}
|
||||
|
||||
let shouldPopSuspenseNode = false;
|
||||
const stashedParent = reconcilingParent;
|
||||
const stashedPrevious = previouslyReconciledSibling;
|
||||
const stashedRemaining = remainingReconcilingChildren;
|
||||
@@ -3987,11 +4070,46 @@ export function attach(
|
||||
previouslyReconciledSiblingSuspenseNode = null;
|
||||
remainingReconcilingChildrenSuspenseNodes =
|
||||
instance.suspenseNode.firstChild;
|
||||
|
||||
shouldPopSuspenseNode = true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Unmount the remaining set.
|
||||
unmountRemainingChildren();
|
||||
if (
|
||||
(instance.kind === FIBER_INSTANCE ||
|
||||
instance.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
instance.data.tag === SuspenseComponent &&
|
||||
OffscreenComponent !== -1
|
||||
) {
|
||||
const fiber = instance.data;
|
||||
const contentFiberInstance = remainingReconcilingChildren;
|
||||
const hydrated = isFiberHydrated(fiber);
|
||||
if (hydrated) {
|
||||
if (contentFiberInstance === null) {
|
||||
throw new Error(
|
||||
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
|
||||
);
|
||||
}
|
||||
|
||||
unmountSuspenseChildrenRecursively(
|
||||
contentFiberInstance,
|
||||
stashedSuspenseParent,
|
||||
stashedSuspensePrevious,
|
||||
stashedSuspenseRemaining,
|
||||
);
|
||||
// unmountSuspenseChildren already popped
|
||||
shouldPopSuspenseNode = false;
|
||||
} else {
|
||||
if (contentFiberInstance !== null) {
|
||||
throw new Error(
|
||||
'A dehydrated Suspense node should not have a content Fiber.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unmountRemainingChildren();
|
||||
}
|
||||
removePreviousSuspendedBy(
|
||||
instance,
|
||||
previousSuspendedBy,
|
||||
@@ -4001,7 +4119,7 @@ export function attach(
|
||||
reconcilingParent = stashedParent;
|
||||
previouslyReconciledSibling = stashedPrevious;
|
||||
remainingReconcilingChildren = stashedRemaining;
|
||||
if (instance.suspenseNode !== null) {
|
||||
if (shouldPopSuspenseNode) {
|
||||
reconcilingParentSuspenseNode = stashedSuspenseParent;
|
||||
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
|
||||
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
|
||||
@@ -4262,7 +4380,9 @@ export function attach(
|
||||
virtualLevel + 1,
|
||||
);
|
||||
if ((updateFlags & ShouldResetChildren) !== NoUpdate) {
|
||||
recordResetChildren(virtualInstance);
|
||||
if (!isInDisconnectedSubtree) {
|
||||
recordResetChildren(virtualInstance);
|
||||
}
|
||||
updateFlags &= ~ShouldResetChildren;
|
||||
}
|
||||
removePreviousSuspendedBy(
|
||||
@@ -5049,7 +5169,9 @@ export function attach(
|
||||
// We need to crawl the subtree for closest non-filtered Fibers
|
||||
// so that we can display them in a flat children set.
|
||||
if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) {
|
||||
recordResetChildren(fiberInstance);
|
||||
if (!nextIsHidden && !isInDisconnectedSubtree) {
|
||||
recordResetChildren(fiberInstance);
|
||||
}
|
||||
|
||||
// We've handled the child order change for this Fiber.
|
||||
// Since it's included, there's no need to invalidate parent child order.
|
||||
@@ -5230,7 +5352,7 @@ export function attach(
|
||||
|
||||
mountFiberRecursively(root.current, false);
|
||||
|
||||
flushPendingEvents(root);
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
currentRoot = (null: any);
|
||||
@@ -5270,12 +5392,12 @@ export function attach(
|
||||
root: FiberRoot,
|
||||
priorityLevel: void | number,
|
||||
) {
|
||||
const current = root.current;
|
||||
const nextFiber = root.current;
|
||||
|
||||
let prevFiber: null | Fiber = null;
|
||||
let rootInstance = rootToFiberInstanceMap.get(root);
|
||||
if (!rootInstance) {
|
||||
rootInstance = createFiberInstance(current);
|
||||
rootInstance = createFiberInstance(nextFiber);
|
||||
rootToFiberInstanceMap.set(root, rootInstance);
|
||||
idToDevToolsInstanceMap.set(rootInstance.id, rootInstance);
|
||||
} else {
|
||||
@@ -5314,30 +5436,28 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
if (prevFiber !== null) {
|
||||
// TODO: relying on this seems a bit fishy.
|
||||
const wasMounted =
|
||||
prevFiber.memoizedState != null &&
|
||||
prevFiber.memoizedState.element != null;
|
||||
const isMounted =
|
||||
current.memoizedState != null && current.memoizedState.element != null;
|
||||
if (!wasMounted && isMounted) {
|
||||
// Mount a new root.
|
||||
setRootPseudoKey(currentRoot.id, current);
|
||||
mountFiberRecursively(current, false);
|
||||
} else if (wasMounted && isMounted) {
|
||||
// Update an existing root.
|
||||
updateFiberRecursively(rootInstance, current, prevFiber, false);
|
||||
} else if (wasMounted && !isMounted) {
|
||||
// Unmount an existing root.
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
removeRootPseudoKey(currentRoot.id);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
}
|
||||
} else {
|
||||
const nextIsMounted = nextFiber.child !== null;
|
||||
const prevWasMounted = prevFiber !== null && prevFiber.child !== null;
|
||||
if (!prevWasMounted && nextIsMounted) {
|
||||
// Mount a new root.
|
||||
setRootPseudoKey(currentRoot.id, current);
|
||||
mountFiberRecursively(current, false);
|
||||
setRootPseudoKey(currentRoot.id, nextFiber);
|
||||
mountFiberRecursively(nextFiber, false);
|
||||
} else if (prevWasMounted && nextIsMounted) {
|
||||
if (prevFiber === null) {
|
||||
throw new Error(
|
||||
'Expected a previous Fiber when updating an existing root.',
|
||||
);
|
||||
}
|
||||
// Update an existing root.
|
||||
updateFiberRecursively(rootInstance, nextFiber, prevFiber, false);
|
||||
} else if (prevWasMounted && !nextIsMounted) {
|
||||
// Unmount an existing root.
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
removeRootPseudoKey(currentRoot.id);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
} else if (!prevWasMounted && !nextIsMounted) {
|
||||
// We don't need this root anymore.
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
}
|
||||
|
||||
if (isProfiling && isProfilingSupported) {
|
||||
@@ -5361,7 +5481,7 @@ export function attach(
|
||||
}
|
||||
|
||||
// We're done here.
|
||||
flushPendingEvents(root);
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
|
||||
@@ -5721,6 +5841,7 @@ export function attach(
|
||||
|
||||
function getSuspendedByOfSuspenseNode(
|
||||
suspenseNode: SuspenseNode,
|
||||
filterByChildInstance: null | DevToolsInstance, // only include suspended by instances in this subtree
|
||||
): Array<SerializedAsyncInfo> {
|
||||
// Collect all ReactAsyncInfo that was suspending this SuspenseNode but
|
||||
// isn't also in any parent set.
|
||||
@@ -5733,6 +5854,15 @@ export function attach(
|
||||
// to a specific instance will have those appear in order of when that instance was discovered.
|
||||
let hooksCacheKey: null | DevToolsInstance = null;
|
||||
let hooksCache: null | HooksTree = null;
|
||||
// Collect the stream entries with the highest byte offset and end time.
|
||||
const streamEntries: Map<
|
||||
Promise<mixed>,
|
||||
{
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
instance: DevToolsInstance,
|
||||
hooks: null | HooksTree,
|
||||
},
|
||||
> = new Map();
|
||||
suspenseNode.suspendedBy.forEach((set, ioInfo) => {
|
||||
let parentNode = suspenseNode.parent;
|
||||
while (parentNode !== null) {
|
||||
@@ -5747,8 +5877,30 @@ export function attach(
|
||||
if (set.size === 0) {
|
||||
return;
|
||||
}
|
||||
const firstInstance: DevToolsInstance = (set.values().next().value: any);
|
||||
if (firstInstance.suspendedBy !== null) {
|
||||
let firstInstance: null | DevToolsInstance = null;
|
||||
if (filterByChildInstance === null) {
|
||||
firstInstance = (set.values().next().value: any);
|
||||
} else {
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const childInstance of set.values()) {
|
||||
if (firstInstance === null) {
|
||||
firstInstance = childInstance;
|
||||
}
|
||||
if (
|
||||
childInstance !== filterByChildInstance &&
|
||||
!isChildOf(
|
||||
filterByChildInstance,
|
||||
childInstance,
|
||||
suspenseNode.instance,
|
||||
)
|
||||
) {
|
||||
// Something suspended on this outside the filtered instance. That means that
|
||||
// it is not unique to just this filtered instance so we skip including it.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstInstance !== null && firstInstance.suspendedBy !== null) {
|
||||
const asyncInfo = getAwaitInSuspendedByFromIO(
|
||||
firstInstance.suspendedBy,
|
||||
ioInfo,
|
||||
@@ -5771,13 +5923,113 @@ export function attach(
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
|
||||
const newIO = asyncInfo.awaited;
|
||||
if (newIO.name === 'RSC stream' && newIO.value != null) {
|
||||
const streamPromise = newIO.value;
|
||||
// Special case RSC stream entries to pick the last entry keyed by the stream.
|
||||
const existingEntry = streamEntries.get(streamPromise);
|
||||
if (existingEntry === undefined) {
|
||||
streamEntries.set(streamPromise, {
|
||||
asyncInfo,
|
||||
instance: firstInstance,
|
||||
hooks,
|
||||
});
|
||||
} else {
|
||||
const existingIO = existingEntry.asyncInfo.awaited;
|
||||
if (
|
||||
newIO !== existingIO &&
|
||||
((newIO.byteSize !== undefined &&
|
||||
existingIO.byteSize !== undefined &&
|
||||
newIO.byteSize > existingIO.byteSize) ||
|
||||
newIO.end > existingIO.end)
|
||||
) {
|
||||
// The new entry is later in the stream that the old entry. Replace it.
|
||||
existingEntry.asyncInfo = asyncInfo;
|
||||
existingEntry.instance = firstInstance;
|
||||
existingEntry.hooks = hooks;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Add any deduped stream entries.
|
||||
streamEntries.forEach(({asyncInfo, instance, hooks}) => {
|
||||
result.push(serializeAsyncInfo(asyncInfo, instance, hooks));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSuspendedByOfInstance(
|
||||
devtoolsInstance: DevToolsInstance,
|
||||
hooks: null | HooksTree,
|
||||
): Array<SerializedAsyncInfo> {
|
||||
const suspendedBy = devtoolsInstance.suspendedBy;
|
||||
if (suspendedBy === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const foundIOEntries: Set<ReactIOInfo> = new Set();
|
||||
const streamEntries: Map<Promise<mixed>, ReactAsyncInfo> = new Map();
|
||||
const result: Array<SerializedAsyncInfo> = [];
|
||||
for (let i = 0; i < suspendedBy.length; i++) {
|
||||
const asyncInfo = suspendedBy[i];
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
if (foundIOEntries.has(ioInfo)) {
|
||||
// We have already added this I/O entry to the result. We can dedupe it.
|
||||
// This can happen when an instance depends on the same data in mutliple places.
|
||||
continue;
|
||||
}
|
||||
foundIOEntries.add(ioInfo);
|
||||
if (ioInfo.name === 'RSC stream' && ioInfo.value != null) {
|
||||
const streamPromise = ioInfo.value;
|
||||
// Special case RSC stream entries to pick the last entry keyed by the stream.
|
||||
const existingEntry = streamEntries.get(streamPromise);
|
||||
if (existingEntry === undefined) {
|
||||
streamEntries.set(streamPromise, asyncInfo);
|
||||
} else {
|
||||
const existingIO = existingEntry.awaited;
|
||||
if (
|
||||
ioInfo !== existingIO &&
|
||||
((ioInfo.byteSize !== undefined &&
|
||||
existingIO.byteSize !== undefined &&
|
||||
ioInfo.byteSize > existingIO.byteSize) ||
|
||||
ioInfo.end > existingIO.end)
|
||||
) {
|
||||
// The new entry is later in the stream that the old entry. Replace it.
|
||||
streamEntries.set(streamPromise, asyncInfo);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
|
||||
}
|
||||
}
|
||||
// Add any deduped stream entries.
|
||||
streamEntries.forEach(asyncInfo => {
|
||||
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSuspendedByOfInstanceSubtree(
|
||||
devtoolsInstance: DevToolsInstance,
|
||||
): Array<SerializedAsyncInfo> {
|
||||
// Get everything suspending below this instance down to the next Suspense node.
|
||||
// First find the parent Suspense boundary which will have accumulated everything
|
||||
let suspenseParentInstance = devtoolsInstance;
|
||||
while (suspenseParentInstance.suspenseNode === null) {
|
||||
if (suspenseParentInstance.parent === null) {
|
||||
// We don't expect to hit this. We should always find the root.
|
||||
return [];
|
||||
}
|
||||
suspenseParentInstance = suspenseParentInstance.parent;
|
||||
}
|
||||
const suspenseNode: SuspenseNode = suspenseParentInstance.suspenseNode;
|
||||
return getSuspendedByOfSuspenseNode(suspenseNode, devtoolsInstance);
|
||||
}
|
||||
|
||||
const FALLBACK_THROTTLE_MS: number = 300;
|
||||
|
||||
function getSuspendedByRange(
|
||||
@@ -6291,17 +6543,17 @@ export function attach(
|
||||
fiberInstance.suspenseNode !== null
|
||||
? // If this is a Suspense boundary, then we include everything in the subtree that might suspend
|
||||
// this boundary down to the next Suspense boundary.
|
||||
getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode)
|
||||
: // This set is an edge case where if you pass a promise to a Client Component into a children
|
||||
// position without a Server Component as the direct parent. E.g. <div>{promise}</div>
|
||||
// In this case, this becomes associated with the Client/Host Component where as normally
|
||||
// you'd expect these to be associated with the Server Component that awaited the data.
|
||||
// TODO: Prepend other suspense sources like css, images and use().
|
||||
fiberInstance.suspendedBy === null
|
||||
? []
|
||||
: fiberInstance.suspendedBy.map(info =>
|
||||
serializeAsyncInfo(info, fiberInstance, hooks),
|
||||
);
|
||||
getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode, null)
|
||||
: tag === ActivityComponent
|
||||
? // For Activity components we show everything that suspends the subtree down to the next boundary
|
||||
// so that you can see what suspends a Transition at that level.
|
||||
getSuspendedByOfInstanceSubtree(fiberInstance)
|
||||
: // This set is an edge case where if you pass a promise to a Client Component into a children
|
||||
// position without a Server Component as the direct parent. E.g. <div>{promise}</div>
|
||||
// In this case, this becomes associated with the Client/Host Component where as normally
|
||||
// you'd expect these to be associated with the Server Component that awaited the data.
|
||||
// TODO: Prepend other suspense sources like css, images and use().
|
||||
getSuspendedByOfInstance(fiberInstance, hooks);
|
||||
const suspendedByRange = getSuspendedByRange(
|
||||
getNearestSuspenseNode(fiberInstance),
|
||||
);
|
||||
@@ -6446,7 +6698,7 @@ export function attach(
|
||||
|
||||
const isSuspended = null;
|
||||
// Things that Suspended this Server Component (use(), awaits and direct child promises)
|
||||
const suspendedBy = virtualInstance.suspendedBy;
|
||||
const suspendedBy = getSuspendedByOfInstance(virtualInstance, null);
|
||||
const suspendedByRange = getSuspendedByRange(
|
||||
getNearestSuspenseNode(virtualInstance),
|
||||
);
|
||||
@@ -6497,12 +6749,7 @@ export function attach(
|
||||
? []
|
||||
: Array.from(componentLogsEntry.warnings.entries()),
|
||||
|
||||
suspendedBy:
|
||||
suspendedBy === null
|
||||
? []
|
||||
: suspendedBy.map(info =>
|
||||
serializeAsyncInfo(info, virtualInstance, null),
|
||||
),
|
||||
suspendedBy: suspendedBy,
|
||||
suspendedByRange: suspendedByRange,
|
||||
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
TREE_OPERATION_REORDER_CHILDREN,
|
||||
SUSPENSE_TREE_OPERATION_ADD,
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
UNKNOWN_SUSPENDERS_NONE,
|
||||
} from '../../constants';
|
||||
import {decorateMany, forceUpdate, restoreMany} from './utils';
|
||||
@@ -411,6 +413,13 @@ export function attach(
|
||||
pushOperation(0); // StrictMode supported?
|
||||
pushOperation(hasOwnerMetadata ? 1 : 0);
|
||||
pushOperation(supportsTogglingSuspense ? 1 : 0);
|
||||
|
||||
pushOperation(SUSPENSE_TREE_OPERATION_ADD);
|
||||
pushOperation(id);
|
||||
pushOperation(parentID);
|
||||
pushOperation(getStringID(null)); // name
|
||||
// TODO: Measure rect of root
|
||||
pushOperation(-1);
|
||||
} else {
|
||||
const type = getElementType(internalInstance);
|
||||
const {displayName, key} = getData(internalInstance);
|
||||
@@ -449,7 +458,12 @@ export function attach(
|
||||
}
|
||||
|
||||
function recordUnmount(internalInstance: InternalInstance, id: number) {
|
||||
pendingUnmountedIDs.push(id);
|
||||
const isRoot = parentIDStack.length === 0;
|
||||
if (isRoot) {
|
||||
pendingUnmountedRootID = id;
|
||||
} else {
|
||||
pendingUnmountedIDs.push(id);
|
||||
}
|
||||
idToInternalInstanceMap.delete(id);
|
||||
}
|
||||
|
||||
@@ -519,6 +533,8 @@ export function attach(
|
||||
// All unmounts are batched in a single message.
|
||||
// [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
|
||||
(numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
|
||||
// [SUSPENSE_TREE_OPERATION_REMOVE, 1, pendingUnmountedRootID]
|
||||
(pendingUnmountedRootID === null ? 0 : 3) +
|
||||
// Mount operations
|
||||
pendingOperations.length,
|
||||
);
|
||||
@@ -555,6 +571,10 @@ export function attach(
|
||||
if (pendingUnmountedRootID !== null) {
|
||||
operations[i] = pendingUnmountedRootID;
|
||||
i++;
|
||||
|
||||
operations[i++] = SUSPENSE_TREE_OPERATION_REMOVE;
|
||||
operations[i++] = 1;
|
||||
operations[i++] = pendingUnmountedRootID;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export const SUSPENSE_TREE_OPERATION_ADD = 8;
|
||||
export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
|
||||
export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
|
||||
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
|
||||
export const SUSPENSE_TREE_OPERATION_SUSPENDERS = 12;
|
||||
|
||||
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
|
||||
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;
|
||||
|
||||
@@ -163,6 +163,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
||||
'--color-scroll-track': '#fafafa',
|
||||
'--color-tooltip-background': 'rgba(0, 0, 0, 0.9)',
|
||||
'--color-tooltip-text': '#ffffff',
|
||||
|
||||
'--elevation-4':
|
||||
'0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)',
|
||||
},
|
||||
dark: {
|
||||
'--color-attribute-name': '#9d87d2',
|
||||
@@ -315,6 +318,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
||||
'--color-scroll-track': '#313640',
|
||||
'--color-tooltip-background': 'rgba(255, 255, 255, 0.95)',
|
||||
'--color-tooltip-text': '#000000',
|
||||
|
||||
'--elevation-4':
|
||||
'0 2px 8px 0 rgba(0,0,0,0.32),0 4px 12px 0 rgba(0,0,0,0.24),0 1px 10px 0 rgba(0,0,0,0.18)',
|
||||
},
|
||||
compact: {
|
||||
'--font-size-monospace-small': '9px',
|
||||
|
||||
122
packages/react-devtools-shared/src/devtools/store.js
vendored
122
packages/react-devtools-shared/src/devtools/store.js
vendored
@@ -24,6 +24,7 @@ import {
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
SUSPENSE_TREE_OPERATION_RESIZE,
|
||||
SUSPENSE_TREE_OPERATION_SUSPENDERS,
|
||||
} from '../constants';
|
||||
import {ElementTypeRoot} from '../frontend/types';
|
||||
import {
|
||||
@@ -111,7 +112,7 @@ export default class Store extends EventEmitter<{
|
||||
roots: [],
|
||||
rootSupportsBasicProfiling: [],
|
||||
rootSupportsTimelineProfiling: [],
|
||||
suspenseTreeMutated: [],
|
||||
suspenseTreeMutated: [[Map<SuspenseNode['id'], SuspenseNode['id']>]],
|
||||
supportsNativeStyleEditor: [],
|
||||
supportsReloadAndProfile: [],
|
||||
unsupportedBridgeProtocolDetected: [],
|
||||
@@ -847,6 +848,83 @@ export default class Store extends EventEmitter<{
|
||||
return list;
|
||||
}
|
||||
|
||||
getSuspenseLineage(
|
||||
suspenseID: SuspenseNode['id'],
|
||||
): $ReadOnlyArray<SuspenseNode['id']> {
|
||||
const lineage: Array<SuspenseNode['id']> = [];
|
||||
let next: null | SuspenseNode = this.getSuspenseByID(suspenseID);
|
||||
while (next !== null) {
|
||||
if (next.parentID === 0) {
|
||||
next = null;
|
||||
} else {
|
||||
lineage.unshift(next.id);
|
||||
next = this.getSuspenseByID(next.parentID);
|
||||
}
|
||||
}
|
||||
|
||||
return lineage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link getRootIDForElement} but should be used for traversing Suspense since it works with disconnected nodes.
|
||||
*/
|
||||
getSuspenseRootIDForSuspense(id: SuspenseNode['id']): number | null {
|
||||
let current = this._idToSuspense.get(id);
|
||||
while (current !== undefined) {
|
||||
if (current.parentID === 0) {
|
||||
return current.id;
|
||||
} else {
|
||||
current = this._idToSuspense.get(current.parentID);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rootID
|
||||
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
|
||||
*/
|
||||
getSuspendableDocumentOrderSuspense(
|
||||
rootID: Element['id'] | void,
|
||||
uniqueSuspendersOnly: boolean,
|
||||
): $ReadOnlyArray<SuspenseNode['id']> {
|
||||
if (rootID === undefined) {
|
||||
return [];
|
||||
}
|
||||
const root = this.getElementByID(rootID);
|
||||
if (root === null) {
|
||||
return [];
|
||||
}
|
||||
if (!this.supportsTogglingSuspense(rootID)) {
|
||||
return [];
|
||||
}
|
||||
const list: SuspenseNode['id'][] = [];
|
||||
const suspense = this.getSuspenseByID(rootID);
|
||||
if (suspense !== null) {
|
||||
const stack = [suspense];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (current === undefined) {
|
||||
continue;
|
||||
}
|
||||
// Include the root even if we won't show it suspended (because that's just blank).
|
||||
// You should be able to see what suspended the shell.
|
||||
if (!uniqueSuspendersOnly || current.hasUniqueSuspenders) {
|
||||
list.push(current.id);
|
||||
}
|
||||
// Add children in reverse order to maintain document order
|
||||
for (let j = current.children.length - 1; j >= 0; j--) {
|
||||
const childSuspense = this.getSuspenseByID(current.children[j]);
|
||||
if (childSuspense !== null) {
|
||||
stack.push(childSuspense);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
getRendererIDForElement(id: number): number | null {
|
||||
let current = this._idToElement.get(id);
|
||||
while (current !== undefined) {
|
||||
@@ -1030,6 +1108,8 @@ export default class Store extends EventEmitter<{
|
||||
const addedElementIDs: Array<number> = [];
|
||||
// This is a mapping of removed ID -> parent ID:
|
||||
const removedElementIDs: Map<number, number> = new Map();
|
||||
const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =
|
||||
new Map();
|
||||
// We'll use the parent ID to adjust selection if it gets deleted.
|
||||
|
||||
let i = 2;
|
||||
@@ -1508,6 +1588,7 @@ export default class Store extends EventEmitter<{
|
||||
children: [],
|
||||
name,
|
||||
rects,
|
||||
hasUniqueSuspenders: false,
|
||||
});
|
||||
|
||||
hasSuspenseTreeChanged = true;
|
||||
@@ -1541,6 +1622,7 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
|
||||
this._idToSuspense.delete(id);
|
||||
removedSuspenseIDs.set(id, parentID);
|
||||
|
||||
let parentSuspense: ?SuspenseNode = null;
|
||||
if (parentID === 0) {
|
||||
@@ -1676,6 +1758,42 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
break;
|
||||
}
|
||||
case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
|
||||
const changeLength = operations[i + 1];
|
||||
i += 2;
|
||||
|
||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||
const id = operations[i];
|
||||
const hasUniqueSuspenders = operations[i + 1] === 1;
|
||||
const suspense = this._idToSuspense.get(id);
|
||||
|
||||
if (suspense === undefined) {
|
||||
this._throwAndEmitError(
|
||||
Error(
|
||||
`Cannot update suspenders of suspense node "${id}" because no matching node was found in the Store.`,
|
||||
),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
i += 2;
|
||||
|
||||
if (__DEBUG__) {
|
||||
const previousHasUniqueSuspenders = suspense.hasUniqueSuspenders;
|
||||
debug(
|
||||
'Suspender changes',
|
||||
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} (was ${String(previousHasUniqueSuspenders)})`,
|
||||
);
|
||||
}
|
||||
|
||||
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
|
||||
}
|
||||
|
||||
hasSuspenseTreeChanged = true;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this._throwAndEmitError(
|
||||
new UnsupportedBridgeOperationError(
|
||||
@@ -1748,7 +1866,7 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
|
||||
if (hasSuspenseTreeChanged) {
|
||||
this.emit('suspenseTreeMutated');
|
||||
this.emit('suspenseTreeMutated', [removedSuspenseIDs]);
|
||||
}
|
||||
|
||||
if (__DEBUG__) {
|
||||
|
||||
@@ -52,7 +52,7 @@ type Props = {
|
||||
type: IconType,
|
||||
};
|
||||
|
||||
const materialIconsViewBox = '0 -960 960 960';
|
||||
const panelIcons = '0 -960 960 820';
|
||||
export default function ButtonIcon({className = '', type}: Props): React.Node {
|
||||
let pathData = null;
|
||||
let viewBox = '0 0 24 24';
|
||||
@@ -131,27 +131,27 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
|
||||
break;
|
||||
case 'panel-left-close':
|
||||
pathData = PATH_MATERIAL_PANEL_LEFT_CLOSE;
|
||||
viewBox = materialIconsViewBox;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'panel-left-open':
|
||||
pathData = PATH_MATERIAL_PANEL_LEFT_OPEN;
|
||||
viewBox = materialIconsViewBox;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'panel-right-close':
|
||||
pathData = PATH_MATERIAL_PANEL_RIGHT_CLOSE;
|
||||
viewBox = materialIconsViewBox;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'panel-right-open':
|
||||
pathData = PATH_MATERIAL_PANEL_RIGHT_OPEN;
|
||||
viewBox = materialIconsViewBox;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'panel-bottom-open':
|
||||
pathData = PATH_MATERIAL_PANEL_BOTTOM_OPEN;
|
||||
viewBox = materialIconsViewBox;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'panel-bottom-close':
|
||||
pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE;
|
||||
viewBox = materialIconsViewBox;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'suspend':
|
||||
pathData = PATH_SUSPEND;
|
||||
|
||||
@@ -14,8 +14,17 @@ import styles from './Badge.css';
|
||||
type Props = {
|
||||
className?: string,
|
||||
children: React$Node,
|
||||
...
|
||||
};
|
||||
|
||||
export default function Badge({className = '', children}: Props): React.Node {
|
||||
return <div className={`${styles.Badge} ${className}`}>{children}</div>;
|
||||
export default function Badge({
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}: Props): React.Node {
|
||||
return (
|
||||
<div {...props} className={`${styles.Badge} ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,22 +8,34 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {useContext} from 'react';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
import {useState, useContext, useCallback} from 'react';
|
||||
|
||||
import SearchInput from '../SearchInput';
|
||||
import SearchInput from 'react-devtools-shared/src/devtools/views/SearchInput';
|
||||
import {
|
||||
TreeDispatcherContext,
|
||||
TreeStateContext,
|
||||
} from 'react-devtools-shared/src/devtools/views/Components/TreeContext';
|
||||
|
||||
type Props = {};
|
||||
export default function ComponentSearchInput(): React.Node {
|
||||
const [localSearchQuery, setLocalSearchQuery] = useState('');
|
||||
const {searchIndex, searchResults} = useContext(TreeStateContext);
|
||||
const transitionDispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
export default function ComponentSearchInput(props: Props): React.Node {
|
||||
const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const search = (text: string) =>
|
||||
dispatch({type: 'SET_SEARCH_TEXT', payload: text});
|
||||
const goToNextResult = () => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'});
|
||||
const goToPreviousResult = () =>
|
||||
dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'});
|
||||
const search = useCallback(
|
||||
(text: string) => {
|
||||
setLocalSearchQuery(text);
|
||||
transitionDispatch({type: 'SET_SEARCH_TEXT', payload: text});
|
||||
},
|
||||
[setLocalSearchQuery, transitionDispatch],
|
||||
);
|
||||
const goToNextResult = useCallback(
|
||||
() => transitionDispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}),
|
||||
[transitionDispatch],
|
||||
);
|
||||
const goToPreviousResult = useCallback(
|
||||
() => transitionDispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}),
|
||||
[transitionDispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchInput
|
||||
@@ -33,7 +45,7 @@ export default function ComponentSearchInput(props: Props): React.Node {
|
||||
search={search}
|
||||
searchIndex={searchIndex}
|
||||
searchResultsCount={searchResults.length}
|
||||
searchText={searchText}
|
||||
searchText={localSearchQuery}
|
||||
testName="ComponentSearchInput"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {Element as ElementType} from 'react-devtools-shared/src/frontend/ty
|
||||
import styles from './Element.css';
|
||||
import Icon from '../Icon';
|
||||
import {useChangeOwnerAction} from './OwnersListContext';
|
||||
import Tooltip from './reach-ui/tooltip';
|
||||
|
||||
type Props = {
|
||||
data: ItemData,
|
||||
@@ -231,15 +232,16 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
/>
|
||||
)}
|
||||
{showStrictModeBadge && (
|
||||
<Icon
|
||||
className={
|
||||
isSelected && treeFocused
|
||||
? styles.StrictModeContrast
|
||||
: styles.StrictMode
|
||||
}
|
||||
title="This component is not running in StrictMode."
|
||||
type="strict-mode-non-compliant"
|
||||
/>
|
||||
<Tooltip label="This component is not running in StrictMode.">
|
||||
<Icon
|
||||
className={
|
||||
isSelected && treeFocused
|
||||
? styles.StrictModeContrast
|
||||
: styles.StrictMode
|
||||
}
|
||||
type="strict-mode-non-compliant"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,11 +11,3 @@
|
||||
position: absolute;
|
||||
right: 0.25em;
|
||||
}
|
||||
|
||||
.ForgetToggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ForgetToggle > span { /* targets .ToggleContent */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user