Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d651f69bc1 | ||
|
|
853550e7c8 | ||
|
|
5cf71b322d | ||
|
|
f807ce6492 | ||
|
|
7b38acca0b | ||
|
|
1d9c3927ea |
@@ -517,14 +517,6 @@ module.exports = {
|
||||
__IS_INTERNAL_VERSION__: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/react-devtools-*/**/*.js'],
|
||||
excludedFiles: '**/__tests__/**/*.js',
|
||||
plugins: ['eslint-plugin-react-hooks-published'],
|
||||
rules: {
|
||||
'react-hooks-published/rules-of-hooks': ERROR,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
|
||||
extends: ['plugin:@typescript-eslint/recommended'],
|
||||
|
||||
11
.github/workflows/compiler_prereleases.yml
vendored
11
.github/workflows/compiler_prereleases.yml
vendored
@@ -19,9 +19,6 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
@@ -58,13 +55,7 @@ jobs:
|
||||
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- if: inputs.dry_run == true
|
||||
name: Publish packages to npm (dry run)
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
- if: inputs.dry_run != true
|
||||
name: Publish packages to npm
|
||||
- name: Publish packages to npm
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
|
||||
@@ -17,9 +17,6 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -36,6 +33,5 @@ jobs:
|
||||
dist_tag: ${{ inputs.dist_tag }}
|
||||
version_name: ${{ inputs.version_name }}
|
||||
tag_version: ${{ inputs.tag_version }}
|
||||
dry_run: ${{ inputs.dry_run }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -19,6 +19,5 @@ jobs:
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
version_name: '0.0.0'
|
||||
dry_run: false
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
49
.github/workflows/devtools_discord_notify.yml
vendored
49
.github/workflows/devtools_discord_notify.yml
vendored
@@ -1,49 +0,0 @@
|
||||
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
|
||||
path: build/devtools.tgz
|
||||
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/playwright-artifacts
|
||||
path: ./tmp/screenshots
|
||||
if-no-files-found: warn
|
||||
|
||||
17
.github/workflows/runtime_build_and_test.yml
vendored
17
.github/workflows/runtime_build_and_test.yml
vendored
@@ -194,7 +194,7 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: |
|
||||
yarn generate-inline-fizz-runtime
|
||||
git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false)
|
||||
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
|
||||
|
||||
# ----- FEATURE FLAGS -----
|
||||
flags:
|
||||
@@ -567,7 +567,7 @@ jobs:
|
||||
- name: Search build artifacts for unminified errors
|
||||
run: |
|
||||
yarn extract-errors
|
||||
git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
|
||||
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
|
||||
|
||||
check_release_dependencies:
|
||||
name: Check release dependencies
|
||||
@@ -766,11 +766,6 @@ 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
|
||||
@@ -781,7 +776,7 @@ jobs:
|
||||
uses: actions/upload-artifact/merge@v4
|
||||
with:
|
||||
name: react-devtools
|
||||
pattern: react-devtools-*
|
||||
pattern: react-devtools-*-extension
|
||||
|
||||
run_devtools_e2e_tests:
|
||||
name: Run DevTools e2e tests
|
||||
@@ -831,12 +826,6 @@ 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,10 +4,8 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
paths-ignore:
|
||||
- packages/react-devtools**
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_**.yml
|
||||
- .github/workflows/devtools**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
4
.github/workflows/runtime_prereleases.yml
vendored
4
.github/workflows/runtime_prereleases.yml
vendored
@@ -82,6 +82,7 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
@@ -90,10 +91,11 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,7 +23,6 @@ chrome-user-data
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
/tmp
|
||||
|
||||
packages/react-devtools-core/dist
|
||||
packages/react-devtools-extensions/chrome/build
|
||||
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
@@ -1,76 +1,3 @@
|
||||
## 19.2.0 (October 1st, 2025)
|
||||
|
||||
Below is a list of all new features, APIs, and bug fixes.
|
||||
|
||||
Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2) for more information.
|
||||
|
||||
### New React Features
|
||||
|
||||
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
|
||||
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
|
||||
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
|
||||
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools
|
||||
|
||||
### New React DOM Features
|
||||
|
||||
- Added resume APIs for partial pre-rendering with Web Streams:
|
||||
- [`resume`](https://react.dev/reference/react-dom/server/resume): to resume a prerender to a stream.
|
||||
- [`resumeAndPrerender`](https://react.dev/reference/react-dom/static/resumeAndPrerender): to resume a prerender to HTML.
|
||||
- Added resume APIs for partial pre-rendering with Node Streams:
|
||||
- [`resumeToPipeableStream`](https://react.dev/reference/react-dom/server/resumeToPipeableStream): to resume a prerender to a stream.
|
||||
- [`resumeAndPrerenderToNodeStream`](https://react.dev/reference/react-dom/static/resumeAndPrerenderToNodeStream): to resume a prerender to HTML.
|
||||
- Updated [`prerender`](https://react.dev/reference/react-dom/static/prerender) APIs to return a `postponed` state that can be passed to the `resume` APIs.
|
||||
|
||||
### Notable changes
|
||||
|
||||
- React DOM now batches suspense boundary reveals, matching the behavior of client side rendering. This change is especially noticeable when animating the reveal of Suspense boundaries e.g. with the upcoming `<ViewTransition>` Component. React will batch as much reveals as possible before the first paint while trying to hit popular first-contentful paint metrics.
|
||||
- Add Node Web Streams (`prerender`, `renderToReadableStream`) to server-side-rendering APIs for Node.js
|
||||
- Use underscore instead of `:` IDs generated by useId
|
||||
|
||||
### All Changes
|
||||
|
||||
#### React
|
||||
|
||||
- `<Activity />` was developed over many years, starting before `ClassComponent.setState` (@acdlite @sebmarkbage and many others)
|
||||
- Stringify context as "SomeContext" instead of "SomeContext.Provider" (@kassens [#33507](https://github.com/facebook/react/pull/33507))
|
||||
- Include stack of cause of React instrumentation errors with `%o` placeholder (@eps1lon [#34198](https://github.com/facebook/react/pull/34198))
|
||||
- Fix infinite `useDeferredValue` loop in popstate event (@acdlite [#32821](https://github.com/facebook/react/pull/32821))
|
||||
- Fix a bug when an initial value was passed to `useDeferredValue` (@acdlite [#34376](https://github.com/facebook/react/pull/34376))
|
||||
- Fix a crash when submitting forms with Client Actions (@sebmarkbage [#33055](https://github.com/facebook/react/pull/33055))
|
||||
- Hide/unhide the content of dehydrated suspense boundaries if they resuspend (@sebmarkbage [#32900](https://github.com/facebook/react/pull/32900))
|
||||
- Avoid stack overflow on wide trees during Hot Reload (@sophiebits [#34145](https://github.com/facebook/react/pull/34145))
|
||||
- Improve Owner and Component stacks in various places (@sebmarkbage, @eps1lon: [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723))
|
||||
- Add `cacheSignal` (@sebmarkbage [#33557](https://github.com/facebook/react/pull/33557))
|
||||
|
||||
#### React DOM
|
||||
|
||||
- Block on Suspensey Fonts during reveal of server-side-rendered content (@sebmarkbage [#33342](https://github.com/facebook/react/pull/33342))
|
||||
- Use underscore instead of `:` for IDs generated by `useId` (@sebmarkbage, @eps1lon: [#32001](https://github.com/facebook/react/pull/32001), [https://github.com/facebook/react/pull/33342](https://github.com/facebook/react/pull/33342)[#33099](https://github.com/facebook/react/pull/33099), [#33422](https://github.com/facebook/react/pull/33422))
|
||||
- Stop warning when ARIA 1.3 attributes are used (@Abdul-Omira [#34264](https://github.com/facebook/react/pull/34264))
|
||||
- Allow `nonce` to be used on hoistable styles (@Andarist [#32461](https://github.com/facebook/react/pull/32461))
|
||||
- Warn for using a React owned node as a Container if it also has text content (@sebmarkbage [#32774](https://github.com/facebook/react/pull/32774))
|
||||
- s/HTML/text for for error messages if text hydration mismatches (@rickhanlonii [#32763](https://github.com/facebook/react/pull/32763))
|
||||
- Fix a bug with `React.use` inside `React.lazy`\-ed Component (@hi-ogawa [#33941](https://github.com/facebook/react/pull/33941))
|
||||
- Enable the `progressiveChunkSize` option for server-side-rendering APIs (@sebmarkbage [#33027](https://github.com/facebook/react/pull/33027))
|
||||
- Fix a bug with deeply nested Suspense inside Suspense fallback when server-side-rendering (@gnoff [#33467](https://github.com/facebook/react/pull/33467))
|
||||
- Avoid hanging when suspending after aborting while rendering (@gnoff [#34192](https://github.com/facebook/react/pull/34192))
|
||||
- Add Node Web Streams to server-side-rendering APIs for Node.js (@sebmarkbage [#33475](https://github.com/facebook/react/pull/33475))
|
||||
|
||||
#### React Server Components
|
||||
|
||||
- Preload `<img>` and `<link>` using hints before they're rendered (@sebmarkbage [#34604](https://github.com/facebook/react/pull/34604))
|
||||
- Log error if production elements are rendered during development (@eps1lon [#34189](https://github.com/facebook/react/pull/34189))
|
||||
- Fix a bug when returning a Temporary reference (e.g. a Client Reference) from Server Functions (@sebmarkbage [#34084](https://github.com/facebook/react/pull/34084), @denk0403 [#33761](https://github.com/facebook/react/pull/33761))
|
||||
- Pass line/column to `filterStackFrame` (@eps1lon [#33707](https://github.com/facebook/react/pull/33707))
|
||||
- Support Async Modules in Turbopack Server References (@lubieowoce [#34531](https://github.com/facebook/react/pull/34531))
|
||||
- Add support for .mjs file extension in Webpack (@jennyscript [#33028](https://github.com/facebook/react/pull/33028))
|
||||
- Fix a wrong missing key warning (@unstubbable [#34350](https://github.com/facebook/react/pull/34350))
|
||||
- Make console log resolve in predictable order (@sebmarkbage [#33665](https://github.com/facebook/react/pull/33665))
|
||||
|
||||
#### React Reconciler
|
||||
|
||||
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
|
||||
|
||||
## 19.1.1 (July 28, 2025)
|
||||
|
||||
### React
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
acdlite
|
||||
eps1lon
|
||||
EugeneChoi4
|
||||
gaearon
|
||||
gnoff
|
||||
unstubbable
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
//
|
||||
// The @latest channel uses the version as-is, e.g.:
|
||||
//
|
||||
// 19.3.0
|
||||
// 19.1.0
|
||||
//
|
||||
// The @canary channel appends additional information, with the scheme
|
||||
// <version>-<label>-<commit_sha>, e.g.:
|
||||
//
|
||||
// 19.3.0-canary-a1c2d3e4
|
||||
// 19.1.0-canary-a1c2d3e4
|
||||
//
|
||||
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
|
||||
//
|
||||
// 0.0.0-experimental-241c4467e-20200129
|
||||
|
||||
const ReactVersion = '19.3.0';
|
||||
const ReactVersion = '19.2.0';
|
||||
|
||||
// The label used by the @canary channel. Represents the upcoming release's
|
||||
// stability. Most of the time, this will be "canary", but we may temporarily
|
||||
@@ -33,8 +33,8 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '7.0.0',
|
||||
'jest-react': '0.18.0',
|
||||
'eslint-plugin-react-hooks': '6.1.0',
|
||||
'jest-react': '0.17.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
'react-dom': ReactVersion,
|
||||
@@ -42,12 +42,12 @@ const stablePackages = {
|
||||
'react-server-dom-turbopack': ReactVersion,
|
||||
'react-server-dom-parcel': ReactVersion,
|
||||
'react-is': ReactVersion,
|
||||
'react-reconciler': '0.34.0',
|
||||
'react-refresh': '0.19.0',
|
||||
'react-reconciler': '0.33.0',
|
||||
'react-refresh': '0.18.0',
|
||||
'react-test-renderer': ReactVersion,
|
||||
'use-subscription': '1.13.0',
|
||||
'use-sync-external-store': '1.7.0',
|
||||
scheduler: '0.28.0',
|
||||
'use-subscription': '1.12.0',
|
||||
'use-sync-external-store': '1.6.0',
|
||||
scheduler: '0.27.0',
|
||||
};
|
||||
|
||||
// These packages do not exist in the @canary or @latest channel, only
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { PluginOptions } from
|
||||
'babel-plugin-react-compiler/dist';
|
||||
({
|
||||
//compilationMode: "all"
|
||||
} satisfies PluginOptions);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export default function TestComponent(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x || true) {
|
||||
t1 = <Button>{x}</Button>;
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
@@ -5,9 +5,8 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {expect, test, type Page} from '@playwright/test';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {encodeStore, type Store} from '../../lib/stores';
|
||||
import {defaultConfig} from '../../lib/defaultStore';
|
||||
import {format} from 'prettier';
|
||||
|
||||
function isMonacoLoaded(): boolean {
|
||||
@@ -21,16 +20,6 @@ function formatPrint(data: Array<string>): Promise<string> {
|
||||
return format(data.join(''), {parser: 'babel'});
|
||||
}
|
||||
|
||||
async function expandConfigs(page: Page): Promise<void> {
|
||||
const expandButton = page.locator('[title="Expand config editor"]');
|
||||
await expandButton.click();
|
||||
await page.waitForSelector('.monaco-editor-config', {state: 'visible'});
|
||||
}
|
||||
|
||||
const TEST_SOURCE = `export default function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}`;
|
||||
|
||||
const TEST_CASE_INPUTS = [
|
||||
{
|
||||
name: 'module-scope-use-memo',
|
||||
@@ -132,9 +121,10 @@ test('editor should open successfully', async ({page}) => {
|
||||
|
||||
test('editor should compile from hash successfully', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
source: `export default function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
`,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
@@ -146,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-output').allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
const output = await formatPrint(text);
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
@@ -155,9 +145,10 @@ test('editor should compile from hash successfully', async ({page}) => {
|
||||
|
||||
test('reset button works', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
source: `export default function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
`,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
@@ -166,201 +157,33 @@ test('reset button works', async ({page}) => {
|
||||
// Reset button works
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
await page.getByRole('button', {name: 'Reset'}).click();
|
||||
await expandConfigs(page);
|
||||
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/02-reset-button-works.png',
|
||||
});
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
const output = await formatPrint(text);
|
||||
|
||||
const configText =
|
||||
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
|
||||
const configOutput = configText.join('');
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
expect(output).toMatchSnapshot('02-default-output.txt');
|
||||
expect(configOutput).not.toEqual('');
|
||||
expect(configOutput).toMatchSnapshot('default-config.txt');
|
||||
});
|
||||
|
||||
test('defaults load when only source is in Store', async ({page}) => {
|
||||
// Test for backwards compatibility
|
||||
const partial = {
|
||||
source: TEST_SOURCE,
|
||||
};
|
||||
const hash = encodeStore(partial as Store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/03-missing-defaults.png',
|
||||
});
|
||||
|
||||
// Config editor has default config
|
||||
const configText =
|
||||
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
|
||||
const configOutput = configText.join('');
|
||||
|
||||
expect(configOutput).not.toEqual('');
|
||||
expect(configOutput).toMatchSnapshot('default-config.txt');
|
||||
|
||||
const checkbox = page.locator('label.show-internals');
|
||||
await expect(checkbox).not.toBeChecked();
|
||||
const ssaTab = page.locator('text=SSA');
|
||||
await expect(ssaTab).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('show internals button toggles correctly', async ({page}) => {
|
||||
await page.goto(`/`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
|
||||
// show internals should be off
|
||||
const checkbox = page.locator('label.show-internals');
|
||||
await checkbox.click();
|
||||
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/04-show-internals-on.png',
|
||||
});
|
||||
|
||||
await expect(checkbox).toBeChecked();
|
||||
|
||||
const ssaTab = page.locator('text=SSA');
|
||||
await expect(ssaTab).toBeVisible();
|
||||
});
|
||||
|
||||
test('error is displayed when config has syntax error', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `compilationMode: `,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/05-config-syntax-error.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = text.join('');
|
||||
|
||||
// Remove hidden chars
|
||||
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
|
||||
});
|
||||
|
||||
test('error is displayed when config has validation error', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
compilationMode: "123"
|
||||
} satisfies PluginOptions);`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/06-config-validation-error.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = text.join('');
|
||||
|
||||
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
|
||||
});
|
||||
|
||||
test('disableMemoizationForDebugging flag works as expected', async ({
|
||||
page,
|
||||
}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
environment: {
|
||||
disableMemoizationForDebugging: true
|
||||
}
|
||||
} satisfies PluginOptions);`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/07-config-disableMemoizationForDebugging-flag.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = await formatPrint(text);
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
|
||||
});
|
||||
|
||||
test('error is displayed when source has syntax error', async ({page}) => {
|
||||
const syntaxErrorSource = `function TestComponent(props) {
|
||||
const oops = props.
|
||||
return (
|
||||
<>{oops}</>
|
||||
);
|
||||
}`;
|
||||
const store: Store = {
|
||||
source: syntaxErrorSource,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`);
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/08-source-syntax-error.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = text.join('');
|
||||
|
||||
expect(output.replace(/\s+/g, ' ')).toContain(
|
||||
'Expected identifier to be defined before being used',
|
||||
);
|
||||
});
|
||||
|
||||
TEST_CASE_INPUTS.forEach((t, idx) =>
|
||||
test(`playground compiles: ${t.name}`, async ({page}) => {
|
||||
const store: Store = {
|
||||
source: t.input,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: `test-results/08-0${idx}-${t.name}.png`,
|
||||
path: `test-results/03-0${idx}-${t.name}.png`,
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
let output: string;
|
||||
if (t.noFormat) {
|
||||
output = text.join('');
|
||||
|
||||
56
compiler/apps/playground/app/index.tsx
Normal file
56
compiler/apps/playground/app/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -1,126 +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 {Resizable} from 're-resizable';
|
||||
import React, {
|
||||
useId,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import {EXPAND_ACCORDION_TRANSITION} from '../lib/transitionTypes';
|
||||
|
||||
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-1 min-w-[550px] sm:min-w-0">
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionWindowItem({
|
||||
name,
|
||||
tabs,
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
const id = useId();
|
||||
const isShow = tabsOpen.has(name);
|
||||
|
||||
const transitionName = `accordion-window-item-${id}`;
|
||||
|
||||
const toggleTabs = (): void => {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
});
|
||||
};
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<ViewTransition
|
||||
name={transitionName}
|
||||
update={{
|
||||
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
|
||||
default: 'none',
|
||||
}}>
|
||||
<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>
|
||||
</ViewTransition>
|
||||
) : (
|
||||
<ViewTransition
|
||||
name={transitionName}
|
||||
update={{
|
||||
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
|
||||
default: 'none',
|
||||
}}>
|
||||
<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>
|
||||
</ViewTransition>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,93 +8,84 @@
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import React, {useState, useCallback} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoConfigOptions} from './monacoOptions';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {
|
||||
ConfigError,
|
||||
generateOverridePragmaFromConfig,
|
||||
updateSourceWithOverridePragma,
|
||||
} from '../../lib/configUtils';
|
||||
|
||||
// @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({
|
||||
formattedAppliedConfig,
|
||||
}: {
|
||||
formattedAppliedConfig: string;
|
||||
}): React.ReactElement {
|
||||
export default function ConfigEditor(): 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={() => {
|
||||
startTransition(() => {
|
||||
addTransitionType(CONFIG_PANEL_TRANSITION);
|
||||
setIsExpanded(false);
|
||||
});
|
||||
}}
|
||||
formattedAppliedConfig={formattedAppliedConfig}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: !isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<CollapsedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
addTransitionType(CONFIG_PANEL_TRANSITION);
|
||||
setIsExpanded(true);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedEditor({
|
||||
onToggle,
|
||||
formattedAppliedConfig,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
formattedAppliedConfig: string;
|
||||
}): React.ReactElement {
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
|
||||
const handleChange: (value: string | undefined) => void = (
|
||||
value: string | undefined,
|
||||
) => {
|
||||
if (value === undefined) return;
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setIsExpanded(prev => !prev);
|
||||
}, []);
|
||||
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
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,
|
||||
);
|
||||
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
dispatchStore({
|
||||
type: 'updateConfig',
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
config: value,
|
||||
source: updatedSource,
|
||||
config: config,
|
||||
},
|
||||
});
|
||||
}, 500); // 500ms debounce delay
|
||||
} 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 => {
|
||||
if (value === undefined) return;
|
||||
|
||||
// Only update the config
|
||||
dispatchStore({
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: store.source,
|
||||
config: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleMount: (
|
||||
@@ -118,98 +109,75 @@ function ExpandedEditor({
|
||||
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});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewTransition
|
||||
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
|
||||
<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}
|
||||
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 border border-gray-300">
|
||||
<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}
|
||||
loading={''}
|
||||
className="monaco-editor-config"
|
||||
options={monacoConfigOptions}
|
||||
/>
|
||||
</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 border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedConfig}
|
||||
loading={''}
|
||||
className="monaco-editor-applied-config"
|
||||
options={{
|
||||
...monacoConfigOptions,
|
||||
readOnly: true,
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
folding: false,
|
||||
renderLineHighlight: 'none',
|
||||
scrollBeyondLastLine: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</Resizable>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsedEditor({
|
||||
onToggle,
|
||||
}: {
|
||||
onToggle: () => 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}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
left: '-8px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="right" className="text-blue-50" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,30 +5,324 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
import {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorCategory,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {useDeferredValue, useMemo, useState} from 'react';
|
||||
import {useStore} from '../StoreContext';
|
||||
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 ConfigEditor from './ConfigEditor';
|
||||
import Input from './Input';
|
||||
import {CompilerOutput, default as Output} from './Output';
|
||||
import {compile} from '../../lib/compilation';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
default as Output,
|
||||
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,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: PluginOptions,
|
||||
): CompilerTransformOutput {
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
[
|
||||
'useFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePaginationFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useRefetchableFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useLazyLoadQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePreloadedQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
results.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
let language: 'flow' | 'typescript';
|
||||
if (source.match(/\@flow/)) {
|
||||
language = 'flow';
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
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);
|
||||
} 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 {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
console.error(err);
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: `Unexpected failure when transforming input! ${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
];
|
||||
}
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
const store = useStore();
|
||||
const deferredStore = useDeferredValue(store);
|
||||
const [compilerOutput, language, appliedOptions] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [compilerOutput, language] = useMemo(
|
||||
() => compile(deferredStore.source, 'compiler'),
|
||||
[deferredStore.source],
|
||||
);
|
||||
const [linterOutput] = useMemo(
|
||||
() => compile(deferredStore.source, 'linter', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
() => compile(deferredStore.source, 'linter'),
|
||||
[deferredStore.source],
|
||||
);
|
||||
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
|
||||
|
||||
// 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>;
|
||||
@@ -42,25 +336,14 @@ export default function Editor(): JSX.Element {
|
||||
mergedOutput = compilerOutput;
|
||||
errors = compilerOutput.error.details;
|
||||
}
|
||||
|
||||
if (appliedOptions) {
|
||||
const formatted = prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
});
|
||||
if (formatted !== formattedAppliedConfig) {
|
||||
setFormattedAppliedConfig(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex top-14">
|
||||
<div className="flex-shrink-0">
|
||||
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
|
||||
</div>
|
||||
<div className="flex flex-1 min-w-0">
|
||||
<div className="relative flex basis top-14">
|
||||
{shouldShowConfig && <ConfigEditor />}
|
||||
<div className={clsx('relative sm:basis-1/4')}>
|
||||
<Input language={language} errors={errors} />
|
||||
</div>
|
||||
<div className={clsx('flex sm:flex flex-wrap')}>
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,31 +6,23 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
|
||||
import invariant from 'invariant';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
} from 'react';
|
||||
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';
|
||||
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
|
||||
|
||||
// @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 | CompilerDiagnostic>;
|
||||
errors: Array<CompilerErrorDetail>;
|
||||
language: 'flow' | 'typescript';
|
||||
};
|
||||
|
||||
@@ -51,6 +43,11 @@ 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(() => {
|
||||
@@ -86,10 +83,14 @@ 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: 'updateSource',
|
||||
type: 'updateFile',
|
||||
payload: {
|
||||
source: value,
|
||||
config,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -139,42 +140,30 @@ 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}
|
||||
className="monaco-editor-input"
|
||||
options={monacoOptions}
|
||||
loading={''}
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = new Map([['Input', editorContent]]);
|
||||
const [activeTab, setActiveTab] = useState('Input');
|
||||
|
||||
return (
|
||||
<ViewTransition
|
||||
update={{
|
||||
[CONFIG_PANEL_TRANSITION]: 'container',
|
||||
default: 'none',
|
||||
}}>
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<div className="flex flex-col h-full !h-[calc(100vh_-_3.5rem)] border-r border-gray-200">
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ViewTransition>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,38 +19,15 @@ import {
|
||||
import parserBabel from 'prettier/plugins/babel';
|
||||
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 {
|
||||
memo,
|
||||
ReactNode,
|
||||
use,
|
||||
useState,
|
||||
Suspense,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
import {
|
||||
CONFIG_PANEL_TRANSITION,
|
||||
TOGGLE_INTERNALS_TRANSITION,
|
||||
EXPAND_ACCORDION_TRANSITION,
|
||||
} from '../../lib/transitionTypes';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
const MemoizedOutput = memo(Output);
|
||||
|
||||
export default MemoizedOutput;
|
||||
|
||||
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
|
||||
|
||||
const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
|
||||
max: 5,
|
||||
});
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'hir';
|
||||
@@ -94,7 +71,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 && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
|
||||
if (!showInternals && passName !== 'Output' && passName !== 'SourceMap') {
|
||||
continue;
|
||||
}
|
||||
for (const result of results) {
|
||||
@@ -219,25 +196,6 @@ ${code}
|
||||
return reorderedTabs;
|
||||
}
|
||||
|
||||
function tabifyCached(
|
||||
store: Store,
|
||||
compilerOutput: CompilerOutput,
|
||||
): Promise<Map<string, ReactNode>> {
|
||||
const cached = tabifyCache.get(store);
|
||||
if (cached) return cached;
|
||||
const result = tabify(store.source, compilerOutput, store.showInternals);
|
||||
tabifyCache.set(store, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function Fallback(): JSX.Element {
|
||||
return (
|
||||
<div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function utf16ToUTF8(s: string): string {
|
||||
return unescape(encodeURIComponent(s));
|
||||
}
|
||||
@@ -251,18 +209,12 @@ function getSourceMapUrl(code: string, map: string): string | null {
|
||||
}
|
||||
|
||||
function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Fallback />}>
|
||||
<OutputContent store={store} compilerOutput={compilerOutput} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
|
||||
() => new Set(['Output']),
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<string>('Output');
|
||||
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
|
||||
() => new Map(),
|
||||
);
|
||||
|
||||
/*
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
@@ -271,19 +223,17 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
const [previousOutputKind, setPreviousOutputKind] = useState(
|
||||
compilerOutput.kind,
|
||||
);
|
||||
const isFailure = compilerOutput.kind !== 'ok';
|
||||
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
if (isFailure) {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
setTabsOpen(prev => new Set(prev).add('Output'));
|
||||
setActiveTab('Output');
|
||||
});
|
||||
}
|
||||
setTabsOpen(new Set(['Output']));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
tabify(store.source, compilerOutput, store.showInternals).then(tabs => {
|
||||
setTabs(tabs);
|
||||
});
|
||||
}, [store.source, compilerOutput, store.showInternals]);
|
||||
|
||||
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
|
||||
let lastResult: string = '';
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
@@ -298,40 +248,17 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
lastResult = currResult;
|
||||
}
|
||||
}
|
||||
const tabs = use(tabifyCached(store, compilerOutput));
|
||||
|
||||
if (!store.showInternals) {
|
||||
return (
|
||||
<ViewTransition
|
||||
update={{
|
||||
[CONFIG_PANEL_TRANSITION]: 'container',
|
||||
[TOGGLE_INTERNALS_TRANSITION]: '',
|
||||
default: 'none',
|
||||
}}>
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewTransition
|
||||
update={{
|
||||
[CONFIG_PANEL_TRANSITION]: 'accordion-container',
|
||||
[TOGGLE_INTERNALS_TRANSITION]: '',
|
||||
default: 'none',
|
||||
}}>
|
||||
<AccordionWindow
|
||||
<>
|
||||
<TabbedWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
</ViewTransition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -383,29 +310,20 @@ function TextTabContent({
|
||||
<DiffEditor
|
||||
original={diff}
|
||||
modified={output}
|
||||
loading={''}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
},
|
||||
dimension: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
readOnly: true,
|
||||
lineNumbers: 'off',
|
||||
glyphMargin: false,
|
||||
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
|
||||
overviewRulerLanes: 0,
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MonacoEditor
|
||||
language={language ?? 'javascript'}
|
||||
value={output}
|
||||
loading={''}
|
||||
className="monaco-editor-output"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
readOnly: true,
|
||||
|
||||
@@ -29,17 +29,4 @@ export const monacoOptions: Partial<EditorProps['options']> = {
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
|
||||
tabSize: 2,
|
||||
};
|
||||
|
||||
export const monacoConfigOptions: Partial<EditorProps['options']> = {
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
};
|
||||
|
||||
@@ -10,16 +10,11 @@ import {CheckIcon} from '@heroicons/react/solid';
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {
|
||||
useState,
|
||||
startTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
} from 'react';
|
||||
import {useState} from 'react';
|
||||
import {defaultStore} from '../lib/defaultStore';
|
||||
import {IconGitHub} from './Icons/IconGitHub';
|
||||
import Logo from './Logo';
|
||||
import {useStore, useStoreDispatch} from './StoreContext';
|
||||
import {TOGGLE_INTERNALS_TRANSITION} from '../lib/transitionTypes';
|
||||
|
||||
export default function Header(): JSX.Element {
|
||||
const [showCheck, setShowCheck] = useState(false);
|
||||
@@ -63,16 +58,11 @@ export default function Header(): JSX.Element {
|
||||
</div>
|
||||
<div className="flex items-center text-[15px] gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="show-internals relative inline-block w-[34px] h-5">
|
||||
<label className="relative inline-block w-[34px] h-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={store.showInternals}
|
||||
onChange={() =>
|
||||
startTransition(() => {
|
||||
addTransitionType(TOGGLE_INTERNALS_TRANSITION);
|
||||
dispatchStore({type: 'toggleInternals'});
|
||||
})
|
||||
}
|
||||
onChange={() => dispatchStore({type: 'toggleInternals'})}
|
||||
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
|
||||
/>
|
||||
<span
|
||||
@@ -82,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-link before:translate-x-3.5'
|
||||
? 'bg-blue-500 before:translate-x-3.5'
|
||||
: 'bg-gray-300',
|
||||
)}></span>
|
||||
</label>
|
||||
|
||||
@@ -1,41 +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 {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,14 +6,10 @@
|
||||
*/
|
||||
|
||||
import type {Dispatch, ReactNode} from 'react';
|
||||
import {useState, useEffect, useReducer} from 'react';
|
||||
import {useEffect, useReducer} from 'react';
|
||||
import createContext from '../lib/createContext';
|
||||
import {emptyStore, defaultStore} from '../lib/defaultStore';
|
||||
import {
|
||||
saveStore,
|
||||
initStoreFromUrlOrLocalStorage,
|
||||
type Store,
|
||||
} from '../lib/stores';
|
||||
import {emptyStore} from '../lib/defaultStore';
|
||||
import {saveStore, type Store} from '../lib/stores';
|
||||
|
||||
const StoreContext = createContext<Store>();
|
||||
|
||||
@@ -34,20 +30,6 @@ 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);
|
||||
@@ -57,7 +39,7 @@ export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
|
||||
return (
|
||||
<StoreContext.Provider value={store}>
|
||||
<StoreDispatchContext.Provider value={dispatch}>
|
||||
{isPageReady ? children : null}
|
||||
{children}
|
||||
</StoreDispatchContext.Provider>
|
||||
</StoreContext.Provider>
|
||||
);
|
||||
@@ -71,14 +53,9 @@ type ReducerAction =
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateSource';
|
||||
type: 'updateFile';
|
||||
payload: {
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'updateConfig';
|
||||
payload: {
|
||||
config: string;
|
||||
};
|
||||
}
|
||||
@@ -92,18 +69,11 @@ function storeReducer(store: Store, action: ReducerAction): Store {
|
||||
const newStore = action.payload.store;
|
||||
return newStore;
|
||||
}
|
||||
case 'updateSource': {
|
||||
const source = action.payload.source;
|
||||
case 'updateFile': {
|
||||
const {source, config} = action.payload;
|
||||
const newStore = {
|
||||
...store,
|
||||
source,
|
||||
};
|
||||
return newStore;
|
||||
}
|
||||
case 'updateConfig': {
|
||||
const config = action.payload.config;
|
||||
const newStore = {
|
||||
...store,
|
||||
config,
|
||||
};
|
||||
return newStore;
|
||||
|
||||
@@ -4,78 +4,103 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import React, {
|
||||
startTransition,
|
||||
useId,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {TOGGLE_TAB_TRANSITION} from '../lib/transitionTypes';
|
||||
|
||||
export default function TabbedWindow({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
}: {
|
||||
tabs: Map<string, React.ReactNode>;
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
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 {
|
||||
const id = useId();
|
||||
const transitionName = `tab-highlight-${id}`;
|
||||
|
||||
const handleTabChange = (tab: string): void => {
|
||||
startTransition(() => {
|
||||
addTransitionType(TOGGLE_TAB_TRANSITION);
|
||||
onTabChange(tab);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<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={() => handleTabChange(tab)}
|
||||
className={clsx(
|
||||
'transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm relative',
|
||||
isActive ? 'text-link' : 'hover:bg-primary/5',
|
||||
)}>
|
||||
{isActive && (
|
||||
<ViewTransition
|
||||
name={transitionName}
|
||||
enter={{default: 'none'}}
|
||||
exit={{default: 'none'}}
|
||||
share={{
|
||||
[TOGGLE_TAB_TRANSITION]: 'tab-highlight',
|
||||
default: 'none',
|
||||
}}
|
||||
update={{default: 'none'}}>
|
||||
<div className="absolute inset-0 bg-highlight rounded-full" />
|
||||
</ViewTransition>
|
||||
)}
|
||||
<ViewTransition
|
||||
enter={{default: 'none'}}
|
||||
exit={{default: 'none'}}
|
||||
update={{
|
||||
[TOGGLE_TAB_TRANSITION]: 'tab-text',
|
||||
default: 'none',
|
||||
}}>
|
||||
<span className="relative z-1">{tab}</span>
|
||||
</ViewTransition>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(activeTab)}
|
||||
</div>
|
||||
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,
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,308 +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 {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorCategory,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import type {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
PrintedCompilerPipelineValue,
|
||||
} from '../components/Editor/Output';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: PluginOptions,
|
||||
): CompilerTransformOutput {
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
[
|
||||
'useFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePaginationFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useRefetchableFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useLazyLoadQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePreloadedQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
results.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
let language: 'flow' | 'typescript';
|
||||
if (source.match(/\@flow/)) {
|
||||
language = 'flow';
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
try {
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} catch (err) {
|
||||
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) {
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* 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
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors() || !transformOutput) {
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
120
compiler/apps/playground/lib/configUtils.ts
Normal file
120
compiler/apps/playground/lib/configUtils.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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,8 +17,23 @@ export const defaultConfig = `\
|
||||
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
//compilationMode: "all"
|
||||
} satisfies PluginOptions);`;
|
||||
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',
|
||||
} satisfies Partial<PluginOptions>);`;
|
||||
|
||||
export const defaultStore: Store = {
|
||||
source: index,
|
||||
|
||||
@@ -71,7 +71,7 @@ export function initStoreFromUrlOrLocalStorage(): Store {
|
||||
// Make sure all properties are populated
|
||||
return {
|
||||
source: raw.source,
|
||||
config: 'config' in raw && raw['config'] ? raw.config : defaultConfig,
|
||||
config: 'config' in raw ? raw.config : defaultConfig,
|
||||
showInternals: 'showInternals' in raw ? raw.showInternals : false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +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.
|
||||
*/
|
||||
|
||||
export const CONFIG_PANEL_TRANSITION = 'config-panel';
|
||||
export const TOGGLE_TAB_TRANSITION = 'toggle-tab';
|
||||
export const TOGGLE_INTERNALS_TRANSITION = 'toggle-internals';
|
||||
export const EXPAND_ACCORDION_TRANSITION = 'open-accordion';
|
||||
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" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -11,7 +11,6 @@ const path = require('path');
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
reactCompiler: true,
|
||||
viewTransition: true,
|
||||
},
|
||||
reactStrictMode: true,
|
||||
webpack: (config, options) => {
|
||||
|
||||
@@ -32,10 +32,9 @@
|
||||
"hermes-eslint": "^0.25.0",
|
||||
"hermes-parser": "^0.25.0",
|
||||
"invariant": "^2.2.4",
|
||||
"lru-cache": "^11.2.2",
|
||||
"lz-string": "^1.5.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"next": "15.6.0-canary.7",
|
||||
"next": "15.5.2",
|
||||
"notistack": "^3.0.0-alpha.7",
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-format": "^29.3.1",
|
||||
@@ -45,7 +44,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
|
||||
@@ -55,16 +55,12 @@ export default defineConfig({
|
||||
// contextOptions: {
|
||||
// ignoreHTTPSErrors: true,
|
||||
// },
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: {width: 1920, height: 1080},
|
||||
},
|
||||
use: {...devices['Desktop Chrome']},
|
||||
},
|
||||
// {
|
||||
// name: 'Desktop Firefox',
|
||||
|
||||
@@ -69,66 +69,3 @@
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(.slide-in) {
|
||||
animation-name: slideOutLeft;
|
||||
}
|
||||
::view-transition-new(.slide-in) {
|
||||
animation-name: slideInLeft;
|
||||
}
|
||||
::view-transition-group(.slide-in) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes slideOutLeft {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(.container),
|
||||
::view-transition-new(.container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::view-transition-old(.accordion-container),
|
||||
::view-transition-new(.accordion-container) {
|
||||
height: 100%;
|
||||
object-fit: none;
|
||||
object-position: left;
|
||||
}
|
||||
|
||||
::view-transition-old(.tab-highlight),
|
||||
::view-transition-new(.tab-highlight) {
|
||||
height: 100%;
|
||||
}
|
||||
::view-transition-group(.tab-text) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::view-transition-old(.expand-accordion),
|
||||
::view-transition-new(.expand-accordion) {
|
||||
width: auto;
|
||||
}
|
||||
::view-transition-group(.expand-accordion) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
/**
|
||||
* For some reason, the original Monaco editor is still visible to the
|
||||
* left of the DiffEditor. This is a workaround for better visual clarity.
|
||||
*/
|
||||
.monaco-diff-editor .editor.original{
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"types": [
|
||||
"react/experimental"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -19,7 +16,7 @@
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -715,10 +715,10 @@
|
||||
dependencies:
|
||||
"@monaco-editor/loader" "^1.4.0"
|
||||
|
||||
"@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/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/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.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-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-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-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-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-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-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-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-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-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-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-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-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-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-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==
|
||||
"@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==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -866,13 +866,6 @@
|
||||
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"
|
||||
@@ -3104,11 +3097,6 @@ lru-cache@^10.2.0:
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||
|
||||
lru-cache@^11.2.2:
|
||||
version "11.2.2"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24"
|
||||
integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==
|
||||
|
||||
lru-cache@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||
@@ -3211,25 +3199,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.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==
|
||||
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==
|
||||
dependencies:
|
||||
"@next/env" "15.6.0-canary.7"
|
||||
"@next/env" "15.5.2"
|
||||
"@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.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"
|
||||
"@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"
|
||||
sharp "^0.34.3"
|
||||
|
||||
node-releases@^2.0.18:
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^2.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"./**/@babel/parser": "7.7.4",
|
||||
|
||||
@@ -17,7 +17,7 @@ export function runBabelPluginReactCompiler(
|
||||
text: string,
|
||||
file: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: PluginOptions | null,
|
||||
options: Partial<PluginOptions> | null,
|
||||
includeAst: boolean = false,
|
||||
): BabelCore.BabelFileResult {
|
||||
const ast = BabelParser.parse(text, {
|
||||
|
||||
@@ -536,8 +536,7 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.StaticComponents:
|
||||
case ErrorCategory.Suppression:
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo:
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
case ErrorCategory.UseMemo: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
@@ -583,10 +582,6 @@ export enum ErrorCategory {
|
||||
* Checking for valid usage of manual memoization
|
||||
*/
|
||||
UseMemo = 'UseMemo',
|
||||
/**
|
||||
* Checking that useMemos always return a value
|
||||
*/
|
||||
VoidUseMemo = 'VoidUseMemo',
|
||||
/**
|
||||
* Checking for higher order functions acting as factories for components/hooks
|
||||
*/
|
||||
@@ -674,21 +669,6 @@ export enum ErrorCategory {
|
||||
FBT = 'FBT',
|
||||
}
|
||||
|
||||
export enum LintRulePreset {
|
||||
/**
|
||||
* Rules that are stable and included in the `recommended` preset.
|
||||
*/
|
||||
Recommended = 'recommended',
|
||||
/**
|
||||
* Rules that are more experimental and only included in the `recommended-latest` preset.
|
||||
*/
|
||||
RecommendedLatest = 'recommended-latest',
|
||||
/**
|
||||
* Rules that are disabled.
|
||||
*/
|
||||
Off = 'off',
|
||||
}
|
||||
|
||||
export type LintRule = {
|
||||
// Stores the category the rule corresponds to, used to filter errors when reporting
|
||||
category: ErrorCategory;
|
||||
@@ -709,14 +689,15 @@ export type LintRule = {
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
|
||||
* any preset.
|
||||
* If true, this rule will automatically appear in the default, "recommended" ESLint
|
||||
* rule set. Otherwise it will be part of an `allRules` export that developers can
|
||||
* use to opt-in to showing output of all possible rules.
|
||||
*
|
||||
* NOTE: not all validations are enabled by default! Setting this flag only affects
|
||||
* whether a given rule is part of the recommended set. The corresponding validation
|
||||
* also should be enabled by default if you want the error to actually show up!
|
||||
*/
|
||||
preset: LintRulePreset;
|
||||
recommended: boolean;
|
||||
};
|
||||
|
||||
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
|
||||
@@ -739,7 +720,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
@@ -749,7 +730,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Config: {
|
||||
@@ -758,7 +739,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
@@ -767,7 +748,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
@@ -777,7 +758,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectSetState: {
|
||||
@@ -787,7 +768,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'set-state-in-effect',
|
||||
description:
|
||||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.ErrorBoundaries: {
|
||||
@@ -797,7 +778,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'error-boundaries',
|
||||
description:
|
||||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Factories: {
|
||||
@@ -808,7 +789,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
description:
|
||||
'Validates against higher order functions defining nested components or hooks. ' +
|
||||
'Components and hooks should be defined at the module level',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
@@ -817,7 +798,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
@@ -826,7 +807,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
@@ -836,7 +817,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Globals: {
|
||||
@@ -847,7 +828,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
description:
|
||||
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
|
||||
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Hooks: {
|
||||
@@ -861,7 +842,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
|
||||
* this rule.
|
||||
*/
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Immutability: {
|
||||
@@ -871,7 +852,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'immutability',
|
||||
description:
|
||||
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Invariant: {
|
||||
@@ -880,7 +861,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.PreserveManualMemo: {
|
||||
@@ -892,7 +873,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
'Validates that existing manual memoized is preserved by the compiler. ' +
|
||||
'React Compiler will only compile components and hooks if its inference ' +
|
||||
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Purity: {
|
||||
@@ -902,7 +883,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'purity',
|
||||
description:
|
||||
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Refs: {
|
||||
@@ -912,7 +893,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'refs',
|
||||
description:
|
||||
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.RenderSetState: {
|
||||
@@ -922,7 +903,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'set-state-in-render',
|
||||
description:
|
||||
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.StaticComponents: {
|
||||
@@ -932,7 +913,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'static-components',
|
||||
description:
|
||||
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Suppression: {
|
||||
@@ -941,7 +922,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Syntax: {
|
||||
@@ -950,7 +931,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Todo: {
|
||||
@@ -959,7 +940,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Hint,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
@@ -969,7 +950,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'unsupported-syntax',
|
||||
description:
|
||||
'Validates against syntax that we do not plan to support in React Compiler',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UseMemo: {
|
||||
@@ -979,17 +960,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'use-memo',
|
||||
description:
|
||||
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'void-use-memo',
|
||||
description:
|
||||
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
@@ -999,7 +970,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'incompatible-library',
|
||||
description:
|
||||
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {ExternalFunction, isHookName} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {LoggerEvent, ParsedPluginOptions} from './Options';
|
||||
import {LoggerEvent, PluginOptions} from './Options';
|
||||
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
|
||||
import {SuppressionRange} from './Suppression';
|
||||
|
||||
@@ -56,7 +56,7 @@ export function validateRestrictedImports(
|
||||
type ProgramContextOptions = {
|
||||
program: NodePath<t.Program>;
|
||||
suppressions: Array<SuppressionRange>;
|
||||
opts: ParsedPluginOptions;
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
hasModuleScopeOptOut: boolean;
|
||||
@@ -66,7 +66,7 @@ export class ProgramContext {
|
||||
* Program and environment context
|
||||
*/
|
||||
scope: BabelScope;
|
||||
opts: ParsedPluginOptions;
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
reactRuntimeModule: string;
|
||||
@@ -240,7 +240,7 @@ export function addImportsToProgram(
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const existingImports = getExistingImports(path);
|
||||
const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
|
||||
const stmts: Array<t.ImportDeclaration> = [];
|
||||
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
@@ -303,29 +303,9 @@ export function addImportsToProgram(
|
||||
if (maybeExistingImports != null) {
|
||||
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
|
||||
} else {
|
||||
if (path.node.sourceType === 'module') {
|
||||
stmts.push(
|
||||
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
|
||||
);
|
||||
} else {
|
||||
stmts.push(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.objectPattern(
|
||||
sortedImport.map(specifier => {
|
||||
return t.objectProperty(
|
||||
t.identifier(specifier.imported),
|
||||
t.identifier(specifier.name),
|
||||
);
|
||||
}),
|
||||
),
|
||||
t.callExpression(t.identifier('require'), [
|
||||
t.stringLiteral(moduleName),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
stmts.push(
|
||||
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
|
||||
);
|
||||
}
|
||||
}
|
||||
path.unshiftContainer('body', stmts);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {z} from 'zod/v4';
|
||||
import {z} from 'zod';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
tryParseExternalFunction,
|
||||
} from '../HIR/Environment';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerPipelineValue} from './Pipeline';
|
||||
|
||||
const PanicThresholdOptionsSchema = z.enum([
|
||||
@@ -51,8 +51,8 @@ const CustomOptOutDirectiveSchema = z
|
||||
.default(null);
|
||||
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
|
||||
|
||||
export type PluginOptions = Partial<{
|
||||
environment: Partial<EnvironmentConfig>;
|
||||
export type PluginOptions = {
|
||||
environment: EnvironmentConfig;
|
||||
|
||||
logger: Logger | null;
|
||||
|
||||
@@ -166,11 +166,7 @@ export type PluginOptions = Partial<{
|
||||
* a userspace approximation of runtime APIs.
|
||||
*/
|
||||
target: CompilerReactTarget;
|
||||
}>;
|
||||
|
||||
export type ParsedPluginOptions = Required<
|
||||
Omit<PluginOptions, 'environment'>
|
||||
> & {environment: EnvironmentConfig};
|
||||
};
|
||||
|
||||
const CompilerReactTargetSchema = z.union([
|
||||
z.literal('17'),
|
||||
@@ -286,7 +282,7 @@ export type Logger = {
|
||||
debugLogIRs?: (value: CompilerPipelineValue) => void;
|
||||
};
|
||||
|
||||
export const defaultOptions: ParsedPluginOptions = {
|
||||
export const defaultOptions: PluginOptions = {
|
||||
compilationMode: 'infer',
|
||||
panicThreshold: 'none',
|
||||
environment: parseEnvironmentConfig({}).unwrap(),
|
||||
@@ -303,9 +299,9 @@ export const defaultOptions: ParsedPluginOptions = {
|
||||
enableReanimatedCheck: true,
|
||||
customOptOutDirectives: null,
|
||||
target: '19',
|
||||
};
|
||||
} as const;
|
||||
|
||||
export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
|
||||
export function parsePluginOptions(obj: unknown): PluginOptions {
|
||||
if (obj == null || typeof obj !== 'object') {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir, env));
|
||||
env.logErrors(validateNoSetStateInEffects(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
|
||||
@@ -23,11 +23,7 @@ import {
|
||||
ProgramContext,
|
||||
validateRestrictedImports,
|
||||
} from './Imports';
|
||||
import {
|
||||
CompilerReactTarget,
|
||||
ParsedPluginOptions,
|
||||
PluginOptions,
|
||||
} from './Options';
|
||||
import {CompilerReactTarget, PluginOptions} from './Options';
|
||||
import {compileFn} from './Pipeline';
|
||||
import {
|
||||
filterSuppressionsThatAffectFunction,
|
||||
@@ -38,7 +34,7 @@ import {GeneratedSource} from '../HIR';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
export type CompilerPass = {
|
||||
opts: ParsedPluginOptions;
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
comments: Array<t.CommentBlock | t.CommentLine>;
|
||||
code: string | null;
|
||||
@@ -49,7 +45,7 @@ const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
|
||||
|
||||
export function tryFindDirectiveEnablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
opts: ParsedPluginOptions,
|
||||
opts: PluginOptions,
|
||||
): Result<t.Directive | null, CompilerError> {
|
||||
const optIn = directives.find(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
@@ -85,7 +81,7 @@ export function findDirectiveDisablingMemoization(
|
||||
}
|
||||
function findDirectivesDynamicGating(
|
||||
directives: Array<t.Directive>,
|
||||
opts: ParsedPluginOptions,
|
||||
opts: PluginOptions,
|
||||
): Result<
|
||||
{
|
||||
gating: ExternalFunction;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import type * as BabelCore from '@babel/core';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {ParsedPluginOptions} from './Options';
|
||||
import {PluginOptions} from './Options';
|
||||
|
||||
function hasModule(name: string): boolean {
|
||||
if (typeof require === 'undefined') {
|
||||
@@ -52,9 +52,7 @@ export function pipelineUsesReanimatedPlugin(
|
||||
return hasModule('react-native-reanimated');
|
||||
}
|
||||
|
||||
export function injectReanimatedFlag(
|
||||
options: ParsedPluginOptions,
|
||||
): ParsedPluginOptions {
|
||||
export function injectReanimatedFlag(options: PluginOptions): PluginOptions {
|
||||
return {
|
||||
...options,
|
||||
environment: {
|
||||
|
||||
@@ -3081,12 +3081,6 @@ 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':
|
||||
|
||||
@@ -454,32 +454,6 @@ function collectNonNullsInBlocks(
|
||||
assumedNonNullObjects.add(entry);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
fn.env.config.enablePreserveExistingMemoizationGuarantees &&
|
||||
instr.value.kind === 'StartMemoize' &&
|
||||
instr.value.deps != null
|
||||
) {
|
||||
for (const dep of instr.value.deps) {
|
||||
if (dep.root.kind === 'NamedLocal') {
|
||||
if (
|
||||
!isImmutableAtInstr(dep.root.value.identifier, instr.id, context)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < dep.path.length; i++) {
|
||||
const pathEntry = dep.path[i]!;
|
||||
if (pathEntry.optional) {
|
||||
break;
|
||||
}
|
||||
const depNode = context.registry.getOrCreateProperty({
|
||||
identifier: dep.root.value.identifier,
|
||||
path: dep.path.slice(0, i),
|
||||
reactive: dep.root.value.reactive,
|
||||
});
|
||||
assumedNonNullObjects.add(depNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,24 +86,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
@@ -159,7 +159,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* A function that, given the name of a module, can optionally return a description
|
||||
* of that module's type signature.
|
||||
*/
|
||||
moduleTypeProvider: z.nullable(z.any()).default(null),
|
||||
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* A list of functions which the application compiles as macros, where
|
||||
@@ -210,7 +210,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* that if a useEffect or useCallback references a function value, that function value will be
|
||||
* considered frozen, and in turn all of its referenced variables will be considered frozen as well.
|
||||
*/
|
||||
enablePreserveExistingMemoizationGuarantees: z.boolean().default(true),
|
||||
enablePreserveExistingMemoizationGuarantees: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that all useMemo/useCallback values are also memoized by Forget. This mode can be
|
||||
@@ -249,7 +249,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Allows specifying a function that can populate HIR with type information from
|
||||
* Flow
|
||||
*/
|
||||
flowTypeProvider: z.nullable(z.any()).default(null),
|
||||
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
@@ -621,13 +621,6 @@ 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.
|
||||
@@ -659,7 +652,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Invalid:
|
||||
* useMemo(() => { ... }, [...]);
|
||||
*/
|
||||
validateNoVoidUseMemo: z.boolean().default(true),
|
||||
validateNoVoidUseMemo: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope
|
||||
@@ -667,13 +660,6 @@ 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>;
|
||||
@@ -906,12 +892,6 @@ export class Environment {
|
||||
if (moduleTypeProvider == null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof moduleTypeProvider !== 'function') {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Expected a function for \`moduleTypeProvider\``,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const unparsedModuleConfig = moduleTypeProvider(moduleName);
|
||||
if (unparsedModuleConfig != null) {
|
||||
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod/v4';
|
||||
import {z} from 'zod';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {isReservedWord} from '../Utils/Keyword';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
@@ -988,7 +988,7 @@ export function createTemporaryPlace(
|
||||
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
|
||||
reactive: false,
|
||||
effect: Effect.Unknown,
|
||||
loc,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {z} from 'zod/v4';
|
||||
import {z} from 'zod';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {
|
||||
EffectSchema,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CallExpression,
|
||||
Effect,
|
||||
Environment,
|
||||
FinishMemoize,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
Place,
|
||||
PropertyLoad,
|
||||
SpreadPattern,
|
||||
StartMemoize,
|
||||
TInstruction,
|
||||
getHookKindForType,
|
||||
makeInstructionId,
|
||||
@@ -182,52 +184,36 @@ function makeManualMemoizationMarkers(
|
||||
depsList: Array<ManualMemoDependency> | null,
|
||||
memoDecl: Place,
|
||||
manualMemoId: number,
|
||||
): [Array<Instruction>, Array<Instruction>] {
|
||||
const temp = createTemporaryPlace(env, memoDecl.loc);
|
||||
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
|
||||
return [
|
||||
[
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env, fnExpr.loc),
|
||||
value: {
|
||||
kind: 'StartMemoize',
|
||||
manualMemoId,
|
||||
/*
|
||||
* Use deps list from source instead of inferred deps
|
||||
* as dependencies
|
||||
*/
|
||||
deps: depsList,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env, fnExpr.loc),
|
||||
value: {
|
||||
kind: 'StartMemoize',
|
||||
manualMemoId,
|
||||
/*
|
||||
* Use deps list from source instead of inferred deps
|
||||
* as dependencies
|
||||
*/
|
||||
deps: depsList,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...temp},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {...memoDecl},
|
||||
loc: memoDecl.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: memoDecl.loc,
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env, fnExpr.loc),
|
||||
value: {
|
||||
kind: 'FinishMemoize',
|
||||
manualMemoId,
|
||||
decl: {...memoDecl},
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env, memoDecl.loc),
|
||||
value: {
|
||||
kind: 'FinishMemoize',
|
||||
manualMemoId,
|
||||
decl: {...temp},
|
||||
loc: memoDecl.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: memoDecl.loc,
|
||||
},
|
||||
],
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -423,7 +409,10 @@ export function dropManualMemoization(
|
||||
* LoadLocal fnArg
|
||||
* - (if validation is enabled) collect manual memoization markers
|
||||
*/
|
||||
const queuedInserts: Map<InstructionId, Array<Instruction>> = new Map();
|
||||
const queuedInserts: Map<
|
||||
InstructionId,
|
||||
TInstruction<StartMemoize> | TInstruction<FinishMemoize>
|
||||
> = new Map();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (let i = 0; i < block.instructions.length; i++) {
|
||||
const instr = block.instructions[i]!;
|
||||
@@ -449,6 +438,40 @@ export function dropManualMemoization(
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bailout on void return useMemos. This is an anti-pattern where code might be using
|
||||
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
|
||||
* values.
|
||||
*/
|
||||
if (
|
||||
func.env.config.validateNoVoidUseMemo &&
|
||||
manualMemo.kind === 'useMemo'
|
||||
) {
|
||||
const funcToCheck = sidemap.functions.get(
|
||||
fnPlace.identifier.id,
|
||||
)?.value;
|
||||
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This ${
|
||||
manualMemo.loadInstr.value.kind === 'PropertyLoad'
|
||||
? 'React.useMemo'
|
||||
: 'useMemo'
|
||||
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instr.value = getManualMemoizationReplacement(
|
||||
fnPlace,
|
||||
instr.value.loc,
|
||||
@@ -534,11 +557,11 @@ export function dropManualMemoization(
|
||||
let nextInstructions: Array<Instruction> | null = null;
|
||||
for (let i = 0; i < block.instructions.length; i++) {
|
||||
const instr = block.instructions[i];
|
||||
const insertInstructions = queuedInserts.get(instr.id);
|
||||
if (insertInstructions != null) {
|
||||
const insertInstr = queuedInserts.get(instr.id);
|
||||
if (insertInstr != null) {
|
||||
nextInstructions = nextInstructions ?? block.instructions.slice(0, i);
|
||||
nextInstructions.push(instr);
|
||||
nextInstructions.push(...insertInstructions);
|
||||
nextInstructions.push(insertInstr);
|
||||
} else if (nextInstructions != null) {
|
||||
nextInstructions.push(instr);
|
||||
}
|
||||
@@ -606,3 +629,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -748,14 +748,10 @@ function applyEffect(
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'MaybeAlias' ||
|
||||
initialized.has(effect.into.identifier.id),
|
||||
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
|
||||
{
|
||||
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)}`,
|
||||
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`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
@@ -771,67 +767,49 @@ function applyEffect(
|
||||
* copy-on-write semantics, then we can prune the effect
|
||||
*/
|
||||
const intoKind = state.kind(effect.into).kind;
|
||||
let destinationType: 'context' | 'mutable' | null = null;
|
||||
let isMutableDesination: boolean;
|
||||
switch (intoKind) {
|
||||
case ValueKind.Context: {
|
||||
destinationType = 'context';
|
||||
break;
|
||||
}
|
||||
case ValueKind.Context:
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
destinationType = 'mutable';
|
||||
isMutableDesination = true;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableDesination = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fromKind = state.kind(effect.from).kind;
|
||||
let sourceType: 'context' | 'mutable' | 'frozen' | null = null;
|
||||
let isMutableReferenceType: boolean;
|
||||
switch (fromKind) {
|
||||
case ValueKind.Context: {
|
||||
sourceType = 'context';
|
||||
break;
|
||||
}
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Primitive: {
|
||||
isMutableReferenceType = false;
|
||||
break;
|
||||
}
|
||||
case ValueKind.Frozen: {
|
||||
sourceType = 'frozen';
|
||||
isMutableReferenceType = false;
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
sourceType = 'mutable';
|
||||
isMutableReferenceType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
) {
|
||||
if (isMutableDesination && isMutableReferenceType) {
|
||||
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;
|
||||
}
|
||||
@@ -1816,16 +1794,8 @@ 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.object.identifier.type.kind === 'Type'
|
||||
value.kind === 'PropertyStore' && value.property === 'current'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
|
||||
@@ -779,13 +779,7 @@ class AliasingState {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({
|
||||
place: edge.node,
|
||||
transitive,
|
||||
direction: 'forwards',
|
||||
// Traversing a maybeAlias edge always downgrades to conditional mutation
|
||||
kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,
|
||||
});
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -813,12 +807,7 @@ 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,
|
||||
|
||||
@@ -7,17 +7,14 @@
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
makeInstructionId,
|
||||
MutableRange,
|
||||
Place,
|
||||
ReactiveScope,
|
||||
ReactiveValue,
|
||||
} from '../HIR';
|
||||
import {Macro, MacroMethod} from '../HIR/Environment';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
import {eachReactiveValueOperand} from './visitors';
|
||||
|
||||
/**
|
||||
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
|
||||
@@ -51,49 +48,24 @@ export function memoizeFbtAndMacroOperandsInSameScope(
|
||||
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
|
||||
...(fn.env.config.customMacros ?? []),
|
||||
]);
|
||||
/**
|
||||
* Set of all identifiers that load fbt or other macro functions or their nested
|
||||
* properties, as well as values known to be the results of invoking macros
|
||||
*/
|
||||
const macroTagsCalls: Set<IdentifierId> = new Set();
|
||||
/**
|
||||
* Mapping of lvalue => list of operands for all expressions where either
|
||||
* the lvalue is a known fbt/macro call and/or the operands transitively
|
||||
* contain fbt/macro calls.
|
||||
*
|
||||
* This is the key data structure that powers the scope merging: we start
|
||||
* at the lvalues and merge operands into the lvalue's scope.
|
||||
*/
|
||||
const macroValues: Map<Identifier, Array<Identifier>> = new Map();
|
||||
// Tracks methods loaded from macros, like fbt.param or idx.foo
|
||||
const fbtValues: Set<IdentifierId> = new Set();
|
||||
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
|
||||
|
||||
visit(fn, fbtMacroTags, macroTagsCalls, macroMethods, macroValues);
|
||||
|
||||
for (const root of macroValues.keys()) {
|
||||
const scope = root.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
while (true) {
|
||||
let vsize = fbtValues.size;
|
||||
let msize = macroMethods.size;
|
||||
visit(fn, fbtMacroTags, fbtValues, macroMethods);
|
||||
if (vsize === fbtValues.size && msize === macroMethods.size) {
|
||||
break;
|
||||
}
|
||||
// Merge the operands into the same scope if this is a known macro invocation
|
||||
if (!macroTagsCalls.has(root.id)) {
|
||||
continue;
|
||||
}
|
||||
mergeScopes(root, scope, macroValues, macroTagsCalls);
|
||||
}
|
||||
|
||||
return macroTagsCalls;
|
||||
return fbtValues;
|
||||
}
|
||||
|
||||
export const FBT_TAGS: Set<string> = new Set([
|
||||
'fbt',
|
||||
'fbt:param',
|
||||
'fbt:enum',
|
||||
'fbt:plural',
|
||||
'fbs',
|
||||
'fbs:param',
|
||||
'fbs:enum',
|
||||
'fbs:plural',
|
||||
]);
|
||||
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||
'fbt:param',
|
||||
@@ -103,22 +75,10 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||
function visit(
|
||||
fn: HIRFunction,
|
||||
fbtMacroTags: Set<Macro>,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
|
||||
macroValues: Map<Identifier, Array<Identifier>>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const macroOperands: Array<Identifier> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
if (macroValues.has(operand.identifier)) {
|
||||
macroOperands.push(operand.identifier);
|
||||
}
|
||||
}
|
||||
if (macroOperands.length !== 0) {
|
||||
macroValues.set(phi.place.identifier, macroOperands);
|
||||
}
|
||||
}
|
||||
for (const instruction of block.instructions) {
|
||||
const {lvalue, value} = instruction;
|
||||
if (lvalue === null) {
|
||||
@@ -133,13 +93,13 @@ function visit(
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
* all `fbt` string literals in case they are used as a jsx tag.
|
||||
*/
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchesExactTag(value.binding.name, fbtMacroTags)
|
||||
) {
|
||||
// Record references to `fbt` as a global
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchTagRoot(value.binding.name, fbtMacroTags) !== null
|
||||
@@ -161,66 +121,84 @@ function visit(
|
||||
if (method.length > 1) {
|
||||
newMethods.push(method.slice(1));
|
||||
} else {
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newMethods.length > 0) {
|
||||
macroMethods.set(lvalue.identifier.id, newMethods);
|
||||
}
|
||||
} else if (
|
||||
value.kind === 'PropertyLoad' &&
|
||||
macroTagsCalls.has(value.object.identifier.id)
|
||||
) {
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
isFbtJsxExpression(fbtMacroTags, macroTagsCalls, value) ||
|
||||
isFbtJsxChild(macroTagsCalls, lvalue, value) ||
|
||||
isFbtCallExpression(macroTagsCalls, value)
|
||||
) {
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
macroValues.set(
|
||||
lvalue.identifier,
|
||||
Array.from(
|
||||
eachInstructionValueOperand(value),
|
||||
operand => operand.identifier,
|
||||
),
|
||||
);
|
||||
} else if (
|
||||
Iterable_some(eachInstructionValueOperand(value), operand =>
|
||||
macroValues.has(operand.identifier),
|
||||
)
|
||||
) {
|
||||
const macroOperands: Array<Identifier> = [];
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (macroValues.has(operand.identifier)) {
|
||||
macroOperands.push(operand.identifier);
|
||||
}
|
||||
} else if (isFbtCallExpression(fbtValues, value)) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* if the JSX element's tag was `fbt`, mark all its operands
|
||||
* to ensure that they end up in the same scope as the jsx element
|
||||
* itself.
|
||||
*/
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
fbtValues.add(operand.identifier.id);
|
||||
}
|
||||
} else if (
|
||||
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
|
||||
isFbtJsxChild(fbtValues, lvalue, value)
|
||||
) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* if the JSX element's tag was `fbt`, mark all its operands
|
||||
* to ensure that they end up in the same scope as the jsx element
|
||||
* itself.
|
||||
*/
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
|
||||
/*
|
||||
* NOTE: we add the operands as fbt values so that they are also
|
||||
* grouped with this expression
|
||||
*/
|
||||
fbtValues.add(operand.identifier.id);
|
||||
}
|
||||
} else if (fbtValues.has(lvalue.identifier.id)) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
if (
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
) {
|
||||
/*
|
||||
* named identifiers were already locals, we only have to force temporaries
|
||||
* into the same scope
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
}
|
||||
macroValues.set(lvalue.identifier, macroOperands);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeScopes(
|
||||
root: Identifier,
|
||||
scope: ReactiveScope,
|
||||
macroValues: Map<Identifier, Array<Identifier>>,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
): void {
|
||||
const operands = macroValues.get(root);
|
||||
if (operands == null) {
|
||||
return;
|
||||
}
|
||||
for (const operand of operands) {
|
||||
operand.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.mutableRange);
|
||||
macroTagsCalls.add(operand.id);
|
||||
mergeScopes(operand, scope, macroValues, macroTagsCalls);
|
||||
}
|
||||
}
|
||||
|
||||
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
|
||||
return Array.from(tags).some(macro =>
|
||||
typeof macro === 'string'
|
||||
@@ -251,40 +229,39 @@ function matchTagRoot(
|
||||
}
|
||||
|
||||
function isFbtCallExpression(
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
value: InstructionValue,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'CallExpression' &&
|
||||
macroTagsCalls.has(value.callee.identifier.id)) ||
|
||||
(value.kind === 'MethodCall' &&
|
||||
macroTagsCalls.has(value.property.identifier.id))
|
||||
fbtValues.has(value.callee.identifier.id)) ||
|
||||
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxExpression(
|
||||
fbtMacroTags: Set<Macro>,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
value: InstructionValue,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
value.kind === 'JsxExpression' &&
|
||||
((value.tag.kind === 'Identifier' &&
|
||||
macroTagsCalls.has(value.tag.identifier.id)) ||
|
||||
fbtValues.has(value.tag.identifier.id)) ||
|
||||
(value.tag.kind === 'BuiltinTag' &&
|
||||
matchesExactTag(value.tag.name, fbtMacroTags)))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxChild(
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
lvalue: Place | null,
|
||||
value: InstructionValue,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
|
||||
lvalue !== null &&
|
||||
macroTagsCalls.has(lvalue.identifier.id)
|
||||
fbtValues.has(lvalue.identifier.id)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,15 +77,6 @@ class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FinishMemoize': {
|
||||
if (
|
||||
!withinScope &&
|
||||
this.alwaysInvalidatingValues.has(value.decl.identifier)
|
||||
) {
|
||||
value.pruned = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {kind: 'keep'};
|
||||
}
|
||||
|
||||
@@ -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 && node.fn.nameHint == null) {
|
||||
if (node.generatedName != 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,10 +70,6 @@ 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': {
|
||||
@@ -110,7 +106,6 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
const variableName = value.lvalue.place.identifier.name;
|
||||
if (
|
||||
node != null &&
|
||||
node.generatedName == null &&
|
||||
variableName != null &&
|
||||
variableName.kind === 'named'
|
||||
) {
|
||||
@@ -142,7 +137,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(arg.identifier.id);
|
||||
if (node != null && node.generatedName == null) {
|
||||
if (node != null) {
|
||||
const generatedName =
|
||||
fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
|
||||
node.generatedName = generatedName;
|
||||
@@ -157,7 +152,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(attr.place.identifier.id);
|
||||
if (node != null && node.generatedName == null) {
|
||||
if (node != null) {
|
||||
const elementName =
|
||||
value.tag.kind === 'BuiltinTag'
|
||||
? value.tag.name
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
BuiltInObjectId,
|
||||
BuiltInPropsId,
|
||||
BuiltInRefValueId,
|
||||
BuiltInSetStateId,
|
||||
BuiltInUseRefId,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
@@ -277,16 +276,9 @@ 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,
|
||||
shapeId: null,
|
||||
return: returnType,
|
||||
isConstructor: false,
|
||||
});
|
||||
@@ -393,7 +385,7 @@ function* generateInstructionTypes(
|
||||
shapeId: BuiltInArrayId,
|
||||
});
|
||||
} else {
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
@@ -175,7 +175,7 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
});
|
||||
}
|
||||
|
||||
const testComplexPluginOptionDefaults: PluginOptions = {
|
||||
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
|
||||
gating: {
|
||||
source: 'ReactForgetFeatureFlag',
|
||||
importSpecifierName: 'isForgetEnabled_Fixtures',
|
||||
@@ -188,6 +188,11 @@ export function parseConfigPragmaForTests(
|
||||
environment?: PartialEnvironmentConfig;
|
||||
},
|
||||
): PluginOptions {
|
||||
const overridePragma = parseConfigPragmaAsString(pragma);
|
||||
if (overridePragma !== '') {
|
||||
return parseConfigStringAsJS(overridePragma, defaults);
|
||||
}
|
||||
|
||||
const environment = parseConfigPragmaEnvironmentForTest(
|
||||
pragma,
|
||||
defaults.environment ?? {},
|
||||
@@ -223,3 +228,100 @@ 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);
|
||||
}
|
||||
|
||||
@@ -5,21 +5,223 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {effect} from 'zod';
|
||||
import {CompilerError, Effect, ErrorSeverity, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
Identifier,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseStateType,
|
||||
IdentifierName,
|
||||
GeneratedSource,
|
||||
} from '../HIR';
|
||||
import {printInstruction} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
eachInstructionLValue,
|
||||
eachPatternOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
// TODO: Maybe I can consolidate some types
|
||||
type SetStateCall = {
|
||||
loc: SourceLocation;
|
||||
invalidDeps: DerivationMetadata;
|
||||
setStateId: IdentifierId;
|
||||
};
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
|
||||
|
||||
type SetStateName = string | undefined | null;
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
// TODO: Rename to place
|
||||
identifierPlace: Place;
|
||||
sources: Place[];
|
||||
};
|
||||
|
||||
// TODO: This needs refining
|
||||
type ErrorMetadata = {
|
||||
errorType: TypeOfValue;
|
||||
invalidDepInfo: string | undefined;
|
||||
loc: SourceLocation;
|
||||
setStateName: SetStateName;
|
||||
};
|
||||
|
||||
function joinValue(
|
||||
lvalueType: TypeOfValue,
|
||||
valueType: TypeOfValue,
|
||||
): TypeOfValue {
|
||||
if (lvalueType === 'ignored') return valueType;
|
||||
if (valueType === 'ignored') return lvalueType;
|
||||
if (lvalueType === valueType) return lvalueType;
|
||||
return 'fromPropsOrState';
|
||||
}
|
||||
|
||||
function updateDerivationMetadata(
|
||||
target: Place,
|
||||
sources: DerivationMetadata[],
|
||||
typeOfValue: TypeOfValue,
|
||||
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
identifierPlace: target,
|
||||
sources: [],
|
||||
typeOfValue: typeOfValue,
|
||||
};
|
||||
|
||||
for (const source of sources) {
|
||||
// If the identifier of the source is a promoted identifier, then
|
||||
// we should set the target as the source.
|
||||
if (source.identifierPlace.identifier.name?.kind === 'promoted') {
|
||||
newValue.sources.push(target);
|
||||
} else {
|
||||
newValue.sources.push(...source.sources);
|
||||
}
|
||||
}
|
||||
derivedTuple.set(target.identifier.id, newValue);
|
||||
}
|
||||
|
||||
function parseInstr(
|
||||
instr: Instruction,
|
||||
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||
setStateCalls: Map<SetStateName, Place[]>,
|
||||
) {
|
||||
// console.log(printInstruction(instr));
|
||||
// console.log(instr);
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
|
||||
// TODO: Not sure if this will catch every time we create a new useState
|
||||
if (
|
||||
instr.value.kind === 'Destructure' &&
|
||||
instr.value.lvalue.pattern.kind === 'ArrayPattern' &&
|
||||
isUseStateType(instr.value.value.identifier)
|
||||
) {
|
||||
const value = instr.value.lvalue.pattern.items[0];
|
||||
if (value.kind === 'Identifier') {
|
||||
derivedTuple.set(value.identifier.id, {
|
||||
identifierPlace: value,
|
||||
sources: [value],
|
||||
typeOfValue: 'fromState',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier' &&
|
||||
instr.value.callee.loc !== GeneratedSource
|
||||
) {
|
||||
if (setStateCalls.has(instr.value.callee.loc.identifierName)) {
|
||||
setStateCalls
|
||||
.get(instr.value.callee.loc.identifierName)!
|
||||
.push(instr.value.callee);
|
||||
} else {
|
||||
setStateCalls.set(instr.value.callee.loc.identifierName, [
|
||||
instr.value.callee,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
let sources: DerivationMetadata[] = [];
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const opSource = derivedTuple.get(operand.identifier.id);
|
||||
if (opSource === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, opSource.typeOfValue);
|
||||
sources.push(opSource);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
updateDerivationMetadata(lvalue, sources, typeOfValue, derivedTuple);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
switch (operand.effect) {
|
||||
case Effect.Capture:
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
if (isMutable(instr, operand)) {
|
||||
updateDerivationMetadata(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
derivedTuple,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: operand.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseBlockPhi(
|
||||
block: BasicBlock,
|
||||
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||
) {
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands.values()) {
|
||||
const source = derivedTuple.get(operand.identifier.id);
|
||||
if (source !== undefined && source.typeOfValue === 'fromProps') {
|
||||
if (
|
||||
source.identifierPlace.identifier.name === null ||
|
||||
source.identifierPlace.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
derivedTuple.set(phi.place.identifier.id, {
|
||||
identifierPlace: phi.place,
|
||||
sources: [phi.place],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
} else {
|
||||
derivedTuple.set(phi.place.identifier.id, {
|
||||
identifierPlace: phi.place,
|
||||
sources: source.sources,
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
@@ -48,12 +250,54 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const derivedTuple: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
const effectSetStates: Map<SetStateName, Place[]> = new Map();
|
||||
const setStateCalls: Map<SetStateName, Place[]> = new Map();
|
||||
|
||||
const errors: ErrorMetadata[] = [];
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
derivedTuple.set(param.identifier.id, {
|
||||
identifierPlace: param,
|
||||
sources: [param],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
derivedTuple.set(props.identifier.id, {
|
||||
identifierPlace: props,
|
||||
sources: [props],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
parseBlockPhi(block, derivedTuple);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
|
||||
parseInstr(instr, derivedTuple, setStateCalls);
|
||||
|
||||
/*
|
||||
* Special case for function expressions, we need to parse nested instructions
|
||||
* TODO: Can there be more recursive levels?
|
||||
*/
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
parseInstr(instr, derivedTuple, setStateCalls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
@@ -66,6 +310,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
@@ -97,6 +342,8 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
derivedTuple,
|
||||
effectSetStates,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
@@ -104,43 +351,99 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
|
||||
const throwableErrors = new CompilerError();
|
||||
for (const error of errors) {
|
||||
let reason;
|
||||
let description = '';
|
||||
// TODO: Not sure if this is robust enough.
|
||||
/*
|
||||
* If we use a setState from an invalid useEffect elsewhere then we probably have to
|
||||
* hoist state up, else we should calculate in render
|
||||
*/
|
||||
if (
|
||||
setStateCalls.get(error.setStateName)?.length !=
|
||||
effectSetStates.get(error.setStateName)?.length &&
|
||||
error.errorType !== 'fromState'
|
||||
) {
|
||||
reason =
|
||||
'Consider lifting state up to the parent component to make this a controlled component. (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)';
|
||||
} else {
|
||||
reason =
|
||||
'You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)';
|
||||
}
|
||||
|
||||
throwableErrors.push({
|
||||
reason: reason,
|
||||
description: `You are using invalid dependencies: \n\n${error.invalidDepInfo}`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: error.loc,
|
||||
});
|
||||
}
|
||||
|
||||
if (throwableErrors.hasAnyErrors()) {
|
||||
throw throwableErrors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||
effectSetStates: Map<SetStateName, Place[]>,
|
||||
errors: ErrorMetadata[],
|
||||
): void {
|
||||
/*
|
||||
* TODO: This makes it so we only capture single line useEffects.
|
||||
* We should be able to capture multiline as well
|
||||
*/
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else if (derivedTuple.has(operand.identifier.id)) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This might be wrong gotta double check
|
||||
let hasInvalidDep = false;
|
||||
for (const dep of effectDeps) {
|
||||
const depMetadata = derivedTuple.get(dep);
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) !=
|
||||
null ||
|
||||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
|
||||
) {
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
hasInvalidDep = true;
|
||||
}
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
if (!hasInvalidDep) {
|
||||
console.log('early return 2');
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
// This variable is suspicious maybe we don't need it?
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
const effectInvalidlyDerived: Map<IdentifierId, DerivationMetadata> =
|
||||
new Map();
|
||||
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
const depMetadata = derivedTuple.get(dep);
|
||||
if (depMetadata !== undefined) {
|
||||
effectInvalidlyDerived.set(dep, depMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
const setStateCallsInEffect: Array<SetStateCall> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -148,21 +451,29 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
|
||||
parseBlockPhi(block, effectInvalidlyDerived);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier' &&
|
||||
instr.value.callee.loc !== GeneratedSource &&
|
||||
instr.value.callee.loc.identifierName !== undefined &&
|
||||
instr.value.callee.loc.identifierName !== null
|
||||
) {
|
||||
if (effectSetStates.has(instr.value.callee.loc.identifierName)) {
|
||||
effectSetStates
|
||||
.get(instr.value.callee.loc.identifierName)!
|
||||
.push(instr.value.callee);
|
||||
} else {
|
||||
effectSetStates.set(instr.value.callee.loc.identifierName, [
|
||||
instr.value.callee,
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
@@ -183,7 +494,7 @@ function validateEffect(
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
@@ -201,38 +512,63 @@ function validateEffect(
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
const invalidDeps = derivedTuple.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (invalidDeps !== undefined) {
|
||||
setStateCallsInEffect.push({
|
||||
loc: instr.value.callee.loc,
|
||||
setStateId: instr.value.callee.identifier.id,
|
||||
invalidDeps: invalidDeps,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log('early return 4');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
for (const call of setStateCallsInEffect) {
|
||||
const placeNames = call.invalidDeps.sources
|
||||
.map(place => place.identifier.name?.value)
|
||||
.join(', ');
|
||||
|
||||
let sourceNames = '';
|
||||
let invalidDepInfo = '';
|
||||
console.log(call.invalidDeps);
|
||||
if (call.invalidDeps.typeOfValue === 'fromProps') {
|
||||
sourceNames += `[${placeNames}], `;
|
||||
sourceNames = sourceNames.slice(0, -2);
|
||||
invalidDepInfo = sourceNames
|
||||
? `Invalid deps from props ${sourceNames}`
|
||||
: '';
|
||||
} else if (call.invalidDeps.typeOfValue === 'fromState') {
|
||||
sourceNames += `[${placeNames}], `;
|
||||
sourceNames = sourceNames.slice(0, -2);
|
||||
invalidDepInfo = sourceNames
|
||||
? `Invalid deps from local state: ${sourceNames}`
|
||||
: '';
|
||||
}
|
||||
|
||||
errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
loc,
|
||||
suggestions: null,
|
||||
errorType: call.invalidDeps.typeOfValue,
|
||||
invalidDepInfo: invalidDepInfo,
|
||||
loc: call.loc,
|
||||
setStateName:
|
||||
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,55 +639,12 @@ 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,23 +11,16 @@ import {
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
Environment,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
isUseRefType,
|
||||
isRefValueType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {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),
|
||||
@@ -39,7 +32,6 @@ import {Iterable_some} from '../Utils/utils';
|
||||
*/
|
||||
export function validateNoSetStateInEffects(
|
||||
fn: HIRFunction,
|
||||
env: Environment,
|
||||
): Result<void, CompilerError> {
|
||||
const setStateFunctions: Map<IdentifierId, Place> = new Map();
|
||||
const errors = new CompilerError();
|
||||
@@ -80,7 +72,6 @@ export function validateNoSetStateInEffects(
|
||||
const callee = getSetStateCall(
|
||||
instr.value.loweredFunc.func,
|
||||
setStateFunctions,
|
||||
env,
|
||||
);
|
||||
if (callee !== null) {
|
||||
setStateFunctions.set(instr.lvalue.identifier.id, callee);
|
||||
@@ -138,42 +129,9 @@ 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)) {
|
||||
@@ -203,21 +161,6 @@ 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)
|
||||
|
||||
@@ -10,37 +10,16 @@ import {
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
const voidMemoErrors = new CompilerError();
|
||||
const useMemos = new Set<IdentifierId>();
|
||||
const react = new Set<IdentifierId>();
|
||||
const functions = new Map<IdentifierId, FunctionExpression>();
|
||||
const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const {lvalue, value} of block.instructions) {
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
/**
|
||||
* Most of the time useMemo results are referenced immediately. Don't bother
|
||||
* scanning instruction operands for useMemos unless there is an as-yet-unused
|
||||
* useMemo.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
unusedUseMemos.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
switch (value.kind) {
|
||||
case 'LoadGlobal': {
|
||||
if (value.binding.name === 'useMemo') {
|
||||
@@ -66,8 +45,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
case 'CallExpression': {
|
||||
// Is the function being called useMemo, with at least 1 argument?
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const isUseMemo = useMemos.has(callee.identifier.id);
|
||||
value.kind === 'CallExpression'
|
||||
? value.callee.identifier.id
|
||||
: value.property.identifier.id;
|
||||
const isUseMemo = useMemos.has(callee);
|
||||
if (!isUseMemo || value.args.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -123,103 +104,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
);
|
||||
}
|
||||
|
||||
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
|
||||
|
||||
if (fn.env.config.validateNoVoidUseMemo) {
|
||||
if (!hasNonVoidReturn(body.loweredFunc.func)) {
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: body.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
unusedUseMemos.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
/**
|
||||
* Basic check for unused memos, where the result of the call is never referenced. This runs
|
||||
* before DCE so it's more of an AST-level check that something, _anything_, cares about the value.
|
||||
*
|
||||
* This is easy to defeat with e.g. `const _ = useMemo(...)` but it at least gives us something to teach.
|
||||
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
|
||||
*/
|
||||
for (const loc of unusedUseMemos.values()) {
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() result is unused',
|
||||
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'useMemo() result is unused',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
fn.env.logErrors(voidMemoErrors.asResult());
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function validateNoContextVariableAssignment(
|
||||
fn: HIRFunction,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const value = instr.value;
|
||||
switch (value.kind) {
|
||||
case 'StoreContext': {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason:
|
||||
'useMemo() callbacks may not reassign variables declared outside of the callback',
|
||||
description:
|
||||
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: value.lvalue.place.loc,
|
||||
message: 'Cannot reassign variable',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
|
||||
validateHooksUsage: 1,
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Invalid input: expected boolean, received number at "validateHooksUsage"."`,
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage"."`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('parseConfigPragma()', () => {
|
||||
],
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: AutodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
// bar(props.b) is an allocating expression that produces a primitive, which means
|
||||
// that Forget should memoize it.
|
||||
// Correctness:
|
||||
@@ -17,8 +16,7 @@ function AllocatingPrimitiveAsDep(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
|
||||
// bar(props.b) is an allocating expression that produces a primitive, which means
|
||||
import { c as _c } from "react/compiler-runtime"; // bar(props.b) is an allocating expression that produces a primitive, which means
|
||||
// that Forget should memoize it.
|
||||
// Correctness:
|
||||
// - y depends on either bar(props.b) or bar(props.b) + 1
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
// bar(props.b) is an allocating expression that produces a primitive, which means
|
||||
// that Forget should memoize it.
|
||||
// Correctness:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
import {useMemo} from 'react';
|
||||
|
||||
const someGlobal = {value: 0};
|
||||
@@ -33,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const someGlobal = { value: 0 };
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
import {useMemo} from 'react';
|
||||
|
||||
const someGlobal = {value: 0};
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
|
||||
## 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)
|
||||
@@ -1,14 +0,0 @@
|
||||
//@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: [{}],
|
||||
};
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
function Component(props) {
|
||||
let a = foo();
|
||||
// freeze `a` so we know the next line cannot mutate it
|
||||
@@ -18,7 +17,7 @@ function Component(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
const a = foo();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
function Component(props) {
|
||||
let a = foo();
|
||||
// freeze `a` so we know the next line cannot mutate it
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
function foo() {
|
||||
@@ -65,7 +64,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, identity } from "shared-runtime";
|
||||
|
||||
function foo() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
function foo() {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
@@ -26,7 +25,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
function foo(props) {
|
||||
let x, y;
|
||||
({x, y} = {x: props.a, y: props.b});
|
||||
@@ -22,7 +21,6 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
function foo(props) {
|
||||
let x;
|
||||
let y;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees:false
|
||||
function foo(props) {
|
||||
let x, y;
|
||||
({x, y} = {x: props.a, y: props.b});
|
||||
|
||||
@@ -29,7 +29,7 @@ Found 1 error:
|
||||
|
||||
Invariant: Expected consistent kind for destructuring
|
||||
|
||||
Other places were `Reassign` but 'mutate? #t8$47[7:9]{reactive}' is const.
|
||||
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
|
||||
|
||||
error.bug-invariant-expected-consistent-destructuring.ts:9:9
|
||||
7 |
|
||||
|
||||
@@ -24,13 +24,15 @@ function BadExample() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
let x;
|
||||
const y = useMemo(() => {
|
||||
let z;
|
||||
x = [];
|
||||
z = true;
|
||||
return z;
|
||||
}, []);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: useMemo() callbacks may not reassign variables declared outside of the callback
|
||||
|
||||
useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
|
||||
|
||||
error.invalid-reassign-variable-in-usememo.ts:5:4
|
||||
3 | const y = useMemo(() => {
|
||||
4 | let z;
|
||||
> 5 | x = [];
|
||||
| ^ Cannot reassign variable
|
||||
6 | z = true;
|
||||
7 | return z;
|
||||
8 | }, []);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
function Component() {
|
||||
let x;
|
||||
const y = useMemo(() => {
|
||||
let z;
|
||||
x = [];
|
||||
z = true;
|
||||
return z;
|
||||
}, []);
|
||||
return [x, y];
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
|
||||
## 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 = {
|
||||
```
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//@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: [{}],
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
## 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 = ... }`
|
||||
```
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//@flow
|
||||
import {useRef} from 'react';
|
||||
|
||||
component C() {
|
||||
const r = useRef(null);
|
||||
if (!r.current) {
|
||||
r.current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: C,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
// Intentionally don't bind state, this repros a bug where we didn't
|
||||
// infer the type of destructured properties after a hole in the array
|
||||
let [, setState] = useState();
|
||||
setState(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
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-setState-in-render-unbound-state.ts:5:2
|
||||
3 | // infer the type of destructured properties after a hole in the array
|
||||
4 | let [, setState] = useState();
|
||||
> 5 | setState(1);
|
||||
| ^^^^^^^^ Found setState() in render
|
||||
6 | return props.foo;
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
function Component(props) {
|
||||
// Intentionally don't bind state, this repros a bug where we didn't
|
||||
// infer the type of destructured properties after a hole in the array
|
||||
let [, setState] = useState();
|
||||
setState(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @flow @enableNewMutationAliasingModel
|
||||
/**
|
||||
* This hook returns a function that when called with an input object,
|
||||
* will return the result of mapping that input with the supplied map
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @flow @enableNewMutationAliasingModel
|
||||
/**
|
||||
* This hook returns a function that when called with an input object,
|
||||
* will return the result of mapping that input with the supplied map
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
|
||||
## 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 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component() {
|
||||
const [state, setState] = useCustomState(0);
|
||||
const aliased = setState;
|
||||
|
||||
setState(1);
|
||||
aliased(2);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function useCustomState(init) {
|
||||
return useState(init);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
## 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 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
|
||||
function Component({setX}) {
|
||||
const aliased = setX;
|
||||
|
||||
setX(1);
|
||||
aliased(2);
|
||||
|
||||
return x;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user