Compare commits
96 Commits
component-
...
pr33572
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
631b1cfc74 | ||
|
|
e1dc03492e | ||
|
|
90bee81902 | ||
|
|
75e78d243f | ||
|
|
5d24c64cc9 | ||
|
|
6b7e207cab | ||
|
|
d60f77a533 | ||
|
|
12bc60f509 | ||
|
|
ed023cfc73 | ||
|
|
a00ca6f6b5 | ||
|
|
888ea60d8e | ||
|
|
b7e2de632b | ||
|
|
ff93c4448c | ||
|
|
6c86e56a0f | ||
|
|
56408a5b12 | ||
|
|
c38e268978 | ||
|
|
80c03eb7e0 | ||
|
|
b6c0aa8814 | ||
|
|
428ab82001 | ||
|
|
4df098c4c2 | ||
|
|
95bcf87e6b | ||
|
|
911dbd9e34 | ||
|
|
c0b5a0cad3 | ||
|
|
e4b88ae4c6 | ||
|
|
6c8bcdaf1b | ||
|
|
b367b60927 | ||
|
|
9666605abf | ||
|
|
65ec57df37 | ||
|
|
b3d5e90786 | ||
|
|
280ff6fed2 | ||
|
|
82f3684c63 | ||
|
|
142aa0744d | ||
|
|
6ccf328499 | ||
|
|
a374e0ec87 | ||
|
|
ab859e31be | ||
|
|
e8d15fa19e | ||
|
|
d177272802 | ||
|
|
22b929156c | ||
|
|
a3be6829c6 | ||
|
|
b1759882c0 | ||
|
|
dddcae7a11 | ||
|
|
43714eb4e9 | ||
|
|
a5110b22f0 | ||
|
|
b4477d3800 | ||
|
|
93f1668045 | ||
|
|
37054867c1 | ||
|
|
d742611ce4 | ||
|
|
1540081725 | ||
|
|
9cc74fec74 | ||
|
|
157ac578de | ||
|
|
45da4e055d | ||
|
|
d8919a0a68 | ||
|
|
2e9f8cd3e0 | ||
|
|
65a46c7eeb | ||
|
|
3fb17d16a4 | ||
|
|
acee65d6d0 | ||
|
|
1ae0a845bd | ||
|
|
2b4064eb9b | ||
|
|
3531b26729 | ||
|
|
4a1f29079c | ||
|
|
526dd340b3 | ||
|
|
ee76351917 | ||
|
|
8b55eb4e72 | ||
|
|
14094f80cb | ||
|
|
5717f1933f | ||
|
|
b07717d857 | ||
|
|
283f87f083 | ||
|
|
f9ae0a4c2e | ||
|
|
f702620cea | ||
|
|
c0464aedb1 | ||
|
|
6a1dfe3777 | ||
|
|
99efc627a5 | ||
|
|
bfaeb4a461 | ||
|
|
3e9db65fc3 | ||
|
|
0d072884f9 | ||
|
|
abf9fd559d | ||
|
|
13f20044f3 | ||
|
|
8ce15b0f56 | ||
|
|
91ac1fea1a | ||
|
|
08064ea671 | ||
|
|
99781d605b | ||
|
|
459a2c4298 | ||
|
|
1c43d0aed7 | ||
|
|
1835b3f7d9 | ||
|
|
f4041aa388 | ||
|
|
3710c4d4f9 | ||
|
|
2388481283 | ||
|
|
9c7b10e22e | ||
|
|
50389e1792 | ||
|
|
99aa685cef | ||
|
|
d38c7e10d3 | ||
|
|
c4676e72a6 | ||
|
|
4c6967be29 | ||
|
|
c6c2a52ad8 | ||
|
|
5dc1b212c3 | ||
|
|
a3abf5f2f8 |
@@ -496,6 +496,7 @@ module.exports = {
|
||||
'packages/react-devtools-shared/src/devtools/views/**/*.js',
|
||||
'packages/react-devtools-shared/src/hook.js',
|
||||
'packages/react-devtools-shared/src/backend/console.js',
|
||||
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
|
||||
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
|
||||
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
|
||||
],
|
||||
@@ -504,6 +505,7 @@ module.exports = {
|
||||
__IS_FIREFOX__: 'readonly',
|
||||
__IS_EDGE__: 'readonly',
|
||||
__IS_NATIVE__: 'readonly',
|
||||
__IS_INTERNAL_MCP_BUILD__: 'readonly',
|
||||
__IS_INTERNAL_VERSION__: 'readonly',
|
||||
chrome: 'readonly',
|
||||
},
|
||||
@@ -559,6 +561,7 @@ module.exports = {
|
||||
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
|
||||
ReturnType: 'readonly',
|
||||
AnimationFrameID: 'readonly',
|
||||
WeakRef: 'readonly',
|
||||
// For Flow type annotation. Only `BigInt` is valid at runtime.
|
||||
bigint: 'readonly',
|
||||
BigInt: 'readonly',
|
||||
@@ -609,6 +612,7 @@ module.exports = {
|
||||
TimeoutID: 'readonly',
|
||||
WheelEventHandler: 'readonly',
|
||||
FinalizationRegistry: 'readonly',
|
||||
Exclude: 'readonly',
|
||||
Omit: 'readonly',
|
||||
Keyframe: 'readonly',
|
||||
PropertyIndexedKeyframes: 'readonly',
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 }}
|
||||
|
||||
29
.github/workflows/runtime_build_and_test.yml
vendored
29
.github/workflows/runtime_build_and_test.yml
vendored
@@ -280,6 +280,35 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
|
||||
|
||||
# Hardcoded to improve parallelism
|
||||
test-linter:
|
||||
name: Test eslint-plugin-react-hooks
|
||||
needs: [runtime_compiler_node_modules_cache]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: |
|
||||
yarn.lock
|
||||
compiler/yarn.lock
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache@v4
|
||||
id: node_modules
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
|
||||
- run: yarn workspace eslint-plugin-react-hooks test
|
||||
|
||||
# ----- BUILD -----
|
||||
build_and_lint:
|
||||
name: yarn build and lint
|
||||
|
||||
1
.github/workflows/runtime_discord_notify.yml
vendored
1
.github/workflows/runtime_discord_notify.yml
vendored
@@ -11,6 +11,7 @@ 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 }}
|
||||
|
||||
41
.github/workflows/runtime_prereleases.yml
vendored
41
.github/workflows/runtime_prereleases.yml
vendored
@@ -17,6 +17,17 @@ on:
|
||||
description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
|
||||
required: false
|
||||
type: boolean
|
||||
only_packages:
|
||||
description: Packages to publish (space separated)
|
||||
type: string
|
||||
skip_packages:
|
||||
description: Packages to NOT publish (space separated)
|
||||
type: string
|
||||
dry:
|
||||
required: true
|
||||
description: Dry run instead of publish?
|
||||
type: boolean
|
||||
default: true
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL:
|
||||
description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
|
||||
@@ -61,10 +72,36 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn --cwd scripts/release install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
- run: |
|
||||
GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }}
|
||||
- name: Check prepared files
|
||||
run: ls -R build/node_modules
|
||||
- if: '${{ inputs.only_packages }}'
|
||||
name: 'Publish ${{ inputs.only_packages }}'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- if: '${{ inputs.skip_packages }}'
|
||||
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
|
||||
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) }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- name: Notify Discord on failure
|
||||
if: failure() && inputs.enableFailureNotification == true
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
|
||||
45
.github/workflows/runtime_prereleases_manual.yml
vendored
45
.github/workflows/runtime_prereleases_manual.yml
vendored
@@ -5,6 +5,25 @@ on:
|
||||
inputs:
|
||||
prerelease_commit_sha:
|
||||
required: true
|
||||
only_packages:
|
||||
description: Packages to publish (space separated)
|
||||
type: string
|
||||
skip_packages:
|
||||
description: Packages to NOT publish (space separated)
|
||||
type: string
|
||||
dry:
|
||||
required: true
|
||||
description: Dry run instead of publish?
|
||||
type: boolean
|
||||
default: true
|
||||
experimental_only:
|
||||
type: boolean
|
||||
description: Only publish to the experimental tag
|
||||
default: false
|
||||
force_notify:
|
||||
description: Force a Discord notification?
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -12,8 +31,26 @@ env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: ${{ github.event.sender.login }}
|
||||
embed-author-url: ${{ github.event.sender.html_url }}
|
||||
embed-author-icon-url: ${{ github.event.sender.avatar_url }}
|
||||
embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}"
|
||||
embed-description: |
|
||||
```json
|
||||
${{ toJson(inputs) }}
|
||||
```
|
||||
embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
|
||||
|
||||
publish_prerelease_canary:
|
||||
if: ${{ !inputs.experimental_only }}
|
||||
name: Publish to Canary channel
|
||||
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
|
||||
permissions:
|
||||
@@ -33,6 +70,9 @@ jobs:
|
||||
# downstream consumers might still expect that tag. We can remove this
|
||||
# after some time has elapsed and the change has been communicated.
|
||||
dist_tag: canary,next
|
||||
only_packages: ${{ inputs.only_packages }}
|
||||
skip_packages: ${{ inputs.skip_packages }}
|
||||
dry: ${{ inputs.dry }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -48,10 +88,15 @@ jobs:
|
||||
# different versions of the same package, even if they use different
|
||||
# dist tags.
|
||||
needs: publish_prerelease_canary
|
||||
# Ensures the job runs even if canary is skipped
|
||||
if: always()
|
||||
with:
|
||||
commit_sha: ${{ inputs.prerelease_commit_sha }}
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
only_packages: ${{ inputs.only_packages }}
|
||||
skip_packages: ${{ inputs.skip_packages }}
|
||||
dry: ${{ inputs.dry }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -22,6 +22,7 @@ jobs:
|
||||
release_channel: stable
|
||||
dist_tag: canary,next
|
||||
enableFailureNotification: true
|
||||
dry: false
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -43,6 +44,7 @@ jobs:
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
enableFailureNotification: true
|
||||
dry: false
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
5
.github/workflows/shared_stale.yml
vendored
5
.github/workflows/shared_stale.yml
vendored
@@ -6,7 +6,10 @@ on:
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
# https://github.com/actions/stale/tree/v9/?tab=readme-ov-file#recommended-permissions
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
|
||||
|
||||
module.exports = {
|
||||
plugins: ['prettier-plugin-hermes-parser'],
|
||||
bracketSpacing: false,
|
||||
singleQuote: true,
|
||||
bracketSameLine: true,
|
||||
trailingComma: 'es5',
|
||||
printWidth: 80,
|
||||
parser: 'hermes',
|
||||
parser: 'flow',
|
||||
arrowParens: 'avoid',
|
||||
overrides: [
|
||||
{
|
||||
|
||||
14
compiler/.gitignore
vendored
14
compiler/.gitignore
vendored
@@ -1,28 +1,14 @@
|
||||
.DS_Store
|
||||
.spr.yml
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
node_modules
|
||||
.watchmanconfig
|
||||
.watchman-cookie-*
|
||||
dist
|
||||
.vscode
|
||||
!packages/playground/.vscode
|
||||
.spr.yml
|
||||
testfilter.txt
|
||||
|
||||
bundle-oss.sh
|
||||
|
||||
# forgive
|
||||
*.vsix
|
||||
.vscode-test
|
||||
|
||||
@@ -8,8 +8,8 @@ set -eo pipefail
|
||||
|
||||
HERE=$(pwd)
|
||||
|
||||
cd ../../packages/react-compiler-runtime && yarn --silent link && cd $HERE
|
||||
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd $HERE
|
||||
cd ../../packages/react-compiler-runtime && yarn --silent link && cd "$HERE"
|
||||
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd "$HERE"
|
||||
|
||||
yarn --silent link babel-plugin-react-compiler
|
||||
yarn --silent link react-compiler-runtime
|
||||
|
||||
@@ -37,6 +37,14 @@ const PanicThresholdOptionsSchema = z.enum([
|
||||
]);
|
||||
|
||||
export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
|
||||
const DynamicGatingOptionsSchema = z.object({
|
||||
source: z.string(),
|
||||
});
|
||||
export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
|
||||
const CustomOptOutDirectiveSchema = z
|
||||
.nullable(z.array(z.string()))
|
||||
.default(null);
|
||||
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
|
||||
|
||||
export type PluginOptions = {
|
||||
environment: EnvironmentConfig;
|
||||
@@ -65,6 +73,28 @@ export type PluginOptions = {
|
||||
*/
|
||||
gating: ExternalFunction | null;
|
||||
|
||||
/**
|
||||
* If specified, this enables dynamic gating which matches `use memo if(...)`
|
||||
* directives.
|
||||
*
|
||||
* Example usage:
|
||||
* ```js
|
||||
* // @dynamicGating:{"source":"myModule"}
|
||||
* export function MyComponent() {
|
||||
* 'use memo if(isEnabled)';
|
||||
* return <div>...</div>;
|
||||
* }
|
||||
* ```
|
||||
* This will emit:
|
||||
* ```js
|
||||
* import {isEnabled} from 'myModule';
|
||||
* export const MyComponent = isEnabled()
|
||||
* ? <optimized version>
|
||||
* : <original version>;
|
||||
* ```
|
||||
*/
|
||||
dynamicGating: DynamicGatingOptions | null;
|
||||
|
||||
panicThreshold: PanicThresholdOptions;
|
||||
|
||||
/*
|
||||
@@ -106,6 +136,11 @@ export type PluginOptions = {
|
||||
*/
|
||||
ignoreUseNoForget: boolean;
|
||||
|
||||
/**
|
||||
* Unstable / do not use
|
||||
*/
|
||||
customOptOutDirectives: CustomOptOutDirective;
|
||||
|
||||
sources: Array<string> | ((filename: string) => boolean) | null;
|
||||
|
||||
/**
|
||||
@@ -244,6 +279,7 @@ export const defaultOptions: PluginOptions = {
|
||||
logger: null,
|
||||
gating: null,
|
||||
noEmit: false,
|
||||
dynamicGating: null,
|
||||
eslintSuppressionRules: null,
|
||||
flowSuppressions: true,
|
||||
ignoreUseNoForget: false,
|
||||
@@ -251,6 +287,7 @@ export const defaultOptions: PluginOptions = {
|
||||
return filename.indexOf('node_modules') === -1;
|
||||
},
|
||||
enableReanimatedCheck: true,
|
||||
customOptOutDirectives: null,
|
||||
target: '19',
|
||||
} as const;
|
||||
|
||||
@@ -292,6 +329,40 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'dynamicGating': {
|
||||
if (value == null) {
|
||||
parsedOptions[key] = null;
|
||||
} else {
|
||||
const result = DynamicGatingOptionsSchema.safeParse(value);
|
||||
if (result.success) {
|
||||
parsedOptions[key] = result.data;
|
||||
} else {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason:
|
||||
'Could not parse dynamic gating. Update React Compiler config to fix the error',
|
||||
description: `${fromZodError(result.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'customOptOutDirectives': {
|
||||
const result = CustomOptOutDirectiveSchema.safeParse(value);
|
||||
if (result.success) {
|
||||
parsedOptions[key] = result.data;
|
||||
} else {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason:
|
||||
'Could not parse custom opt out directives. Update React Compiler config to fix the error',
|
||||
description: `${fromZodError(result.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
parsedOptions[key] = value;
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ function run(
|
||||
mode,
|
||||
config,
|
||||
contextIdentifiers,
|
||||
func,
|
||||
logger,
|
||||
filename,
|
||||
code,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CompilerErrorDetail,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {ReactFunctionType} from '../HIR/Environment';
|
||||
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
|
||||
import {isHookDeclaration} from '../Utils/HookDeclaration';
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
suppressionsToCompilerError,
|
||||
} from './Suppression';
|
||||
import {GeneratedSource} from '../HIR';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
export type CompilerPass = {
|
||||
opts: PluginOptions;
|
||||
@@ -40,26 +41,102 @@ export type CompilerPass = {
|
||||
};
|
||||
export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
|
||||
export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
|
||||
const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
|
||||
|
||||
export function findDirectiveEnablingMemoization(
|
||||
export function tryFindDirectiveEnablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
): t.Directive | null {
|
||||
return (
|
||||
directives.find(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
) ?? null
|
||||
opts: PluginOptions,
|
||||
): Result<t.Directive | null, CompilerError> {
|
||||
const optIn = directives.find(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
);
|
||||
if (optIn != null) {
|
||||
return Ok(optIn);
|
||||
}
|
||||
const dynamicGating = findDirectivesDynamicGating(directives, opts);
|
||||
if (dynamicGating.isOk()) {
|
||||
return Ok(dynamicGating.unwrap()?.directive ?? null);
|
||||
} else {
|
||||
return Err(dynamicGating.unwrapErr());
|
||||
}
|
||||
}
|
||||
|
||||
export function findDirectiveDisablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
{customOptOutDirectives}: PluginOptions,
|
||||
): t.Directive | null {
|
||||
if (customOptOutDirectives != null) {
|
||||
return (
|
||||
directives.find(
|
||||
directive =>
|
||||
customOptOutDirectives.indexOf(directive.value.value) !== -1,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
return (
|
||||
directives.find(directive =>
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
function findDirectivesDynamicGating(
|
||||
directives: Array<t.Directive>,
|
||||
opts: PluginOptions,
|
||||
): Result<
|
||||
{
|
||||
gating: ExternalFunction;
|
||||
directive: t.Directive;
|
||||
} | null,
|
||||
CompilerError
|
||||
> {
|
||||
if (opts.dynamicGating === null) {
|
||||
return Ok(null);
|
||||
}
|
||||
const errors = new CompilerError();
|
||||
const result: Array<{directive: t.Directive; match: string}> = [];
|
||||
|
||||
for (const directive of directives) {
|
||||
const maybeMatch = DYNAMIC_GATING_DIRECTIVE.exec(directive.value.value);
|
||||
if (maybeMatch != null && maybeMatch[1] != null) {
|
||||
if (t.isValidIdentifier(maybeMatch[1])) {
|
||||
result.push({directive, match: maybeMatch[1]});
|
||||
} else {
|
||||
errors.push({
|
||||
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
|
||||
description: `Found '${directive.value.value}'`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
return Err(errors);
|
||||
} else if (result.length > 1) {
|
||||
const error = new CompilerError();
|
||||
error.push({
|
||||
reason: `Multiple dynamic gating directives found`,
|
||||
description: `Expected a single directive but found [${result
|
||||
.map(r => r.directive.value.value)
|
||||
.join(', ')}]`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: result[0].directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
return Err(error);
|
||||
} else if (result.length === 1) {
|
||||
return Ok({
|
||||
gating: {
|
||||
source: opts.dynamicGating.source,
|
||||
importSpecifierName: result[0].match,
|
||||
},
|
||||
directive: result[0].directive,
|
||||
});
|
||||
} else {
|
||||
return Ok(null);
|
||||
}
|
||||
}
|
||||
|
||||
function isCriticalError(err: unknown): boolean {
|
||||
return !(err instanceof CompilerError) || err.isCritical();
|
||||
@@ -326,7 +403,8 @@ export function compileProgram(
|
||||
code: pass.code,
|
||||
suppressions,
|
||||
hasModuleScopeOptOut:
|
||||
findDirectiveDisablingMemoization(program.node.directives) != null,
|
||||
findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=
|
||||
null,
|
||||
});
|
||||
|
||||
const queue: Array<CompileSource> = findFunctionsToCompile(
|
||||
@@ -477,13 +555,36 @@ function processFn(
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
): null | CodegenFunction {
|
||||
let directives;
|
||||
let directives: {
|
||||
optIn: t.Directive | null;
|
||||
optOut: t.Directive | null;
|
||||
};
|
||||
if (fn.node.body.type !== 'BlockStatement') {
|
||||
directives = {optIn: null, optOut: null};
|
||||
} else {
|
||||
directives = {
|
||||
optIn: findDirectiveEnablingMemoization(fn.node.body.directives),
|
||||
optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
|
||||
optIn: null,
|
||||
optOut: null,
|
||||
};
|
||||
} else {
|
||||
const optIn = tryFindDirectiveEnablingMemoization(
|
||||
fn.node.body.directives,
|
||||
programContext.opts,
|
||||
);
|
||||
if (optIn.isErr()) {
|
||||
/**
|
||||
* If parsing opt-in directive fails, it's most likely that React Compiler
|
||||
* was not tested or rolled out on this function. In that case, we handle
|
||||
* the error and fall back to the safest option which is to not optimize
|
||||
* the function.
|
||||
*/
|
||||
handleError(optIn.unwrapErr(), programContext, fn.node.loc ?? null);
|
||||
return null;
|
||||
}
|
||||
directives = {
|
||||
optIn: optIn.unwrapOr(null),
|
||||
optOut: findDirectiveDisablingMemoization(
|
||||
fn.node.body.directives,
|
||||
programContext.opts,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -659,25 +760,31 @@ function applyCompiledFunctions(
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const referencedBeforeDeclared =
|
||||
pass.opts.gating != null
|
||||
? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
|
||||
: null;
|
||||
let referencedBeforeDeclared = null;
|
||||
for (const result of compiledFns) {
|
||||
const {kind, originalFn, compiledFn} = result;
|
||||
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
|
||||
programContext.alreadyCompiled.add(transformedFn);
|
||||
|
||||
if (referencedBeforeDeclared != null && kind === 'original') {
|
||||
CompilerError.invariant(pass.opts.gating != null, {
|
||||
reason: "Expected 'gating' import to be present",
|
||||
loc: null,
|
||||
});
|
||||
let dynamicGating: ExternalFunction | null = null;
|
||||
if (originalFn.node.body.type === 'BlockStatement') {
|
||||
const result = findDirectivesDynamicGating(
|
||||
originalFn.node.body.directives,
|
||||
pass.opts,
|
||||
);
|
||||
if (result.isOk()) {
|
||||
dynamicGating = result.unwrap()?.gating ?? null;
|
||||
}
|
||||
}
|
||||
const functionGating = dynamicGating ?? pass.opts.gating;
|
||||
if (kind === 'original' && functionGating != null) {
|
||||
referencedBeforeDeclared ??=
|
||||
getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns);
|
||||
insertGatedFunctionDeclaration(
|
||||
originalFn,
|
||||
transformedFn,
|
||||
programContext,
|
||||
pass.opts.gating,
|
||||
functionGating,
|
||||
referencedBeforeDeclared.has(result),
|
||||
);
|
||||
} else {
|
||||
@@ -733,8 +840,13 @@ function getReactFunctionType(
|
||||
): ReactFunctionType | null {
|
||||
const hookPattern = pass.opts.environment.hookPattern;
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
if (findDirectiveEnablingMemoization(fn.node.body.directives) != null)
|
||||
const optInDirectives = tryFindDirectiveEnablingMemoization(
|
||||
fn.node.body.directives,
|
||||
pass.opts,
|
||||
);
|
||||
if (optInDirectives.unwrapOr(null) != null) {
|
||||
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
|
||||
}
|
||||
}
|
||||
|
||||
// Component and hook declarations are known components/hooks
|
||||
|
||||
@@ -70,12 +70,14 @@ import {BuiltInArrayId} from './ObjectShape';
|
||||
export function lower(
|
||||
func: NodePath<t.Function>,
|
||||
env: Environment,
|
||||
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
|
||||
bindings: Bindings | null = null,
|
||||
capturedRefs: Array<t.Identifier> = [],
|
||||
// the outermost function being compiled, in case lower() is called recursively (for lambdas)
|
||||
parent: NodePath<t.Function> | null = null,
|
||||
): Result<HIRFunction, CompilerError> {
|
||||
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
|
||||
const builder = new HIRBuilder(env, {
|
||||
bindings,
|
||||
context: capturedRefs,
|
||||
});
|
||||
const context: HIRFunction['context'] = [];
|
||||
|
||||
for (const ref of capturedRefs ?? []) {
|
||||
@@ -215,7 +217,7 @@ export function lower(
|
||||
return Ok({
|
||||
id,
|
||||
params,
|
||||
fnType: parent == null ? env.fnType : 'Other',
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
body: builder.build(),
|
||||
@@ -3417,7 +3419,7 @@ function lowerFunction(
|
||||
| t.ObjectMethod
|
||||
>,
|
||||
): LoweredFunction | null {
|
||||
const componentScope: Scope = builder.parentFunction.scope;
|
||||
const componentScope: Scope = builder.environment.parentFunction.scope;
|
||||
const capturedContext = gatherCapturedContext(expr, componentScope);
|
||||
|
||||
/*
|
||||
@@ -3428,13 +3430,10 @@ function lowerFunction(
|
||||
* This isn't a problem in practice because use Babel's scope analysis to
|
||||
* identify the correct references.
|
||||
*/
|
||||
const lowering = lower(
|
||||
expr,
|
||||
builder.environment,
|
||||
builder.bindings,
|
||||
[...builder.context, ...capturedContext],
|
||||
builder.parentFunction,
|
||||
);
|
||||
const lowering = lower(expr, builder.environment, builder.bindings, [
|
||||
...builder.context,
|
||||
...capturedContext,
|
||||
]);
|
||||
let loweredFunc: HIRFunction;
|
||||
if (lowering.isErr()) {
|
||||
lowering
|
||||
@@ -3456,7 +3455,7 @@ function lowerExpressionToTemporary(
|
||||
return lowerValueToTemporary(builder, value);
|
||||
}
|
||||
|
||||
function lowerValueToTemporary(
|
||||
export function lowerValueToTemporary(
|
||||
builder: HIRBuilder,
|
||||
value: InstructionValue,
|
||||
): Place {
|
||||
|
||||
@@ -241,7 +241,10 @@ type PropertyPathNode =
|
||||
class PropertyPathRegistry {
|
||||
roots: Map<IdentifierId, RootNode> = new Map();
|
||||
|
||||
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
|
||||
getOrCreateIdentifier(
|
||||
identifier: Identifier,
|
||||
reactive: boolean,
|
||||
): PropertyPathNode {
|
||||
/**
|
||||
* Reads from a statically scoped variable are always safe in JS,
|
||||
* with the exception of TDZ (not addressed by this pass).
|
||||
@@ -255,12 +258,19 @@ class PropertyPathRegistry {
|
||||
optionalProperties: new Map(),
|
||||
fullPath: {
|
||||
identifier,
|
||||
reactive,
|
||||
path: [],
|
||||
},
|
||||
hasOptional: false,
|
||||
parent: null,
|
||||
};
|
||||
this.roots.set(identifier.id, rootNode);
|
||||
} else {
|
||||
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
|
||||
reason:
|
||||
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
|
||||
loc: identifier.loc,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
@@ -278,6 +288,7 @@ class PropertyPathRegistry {
|
||||
parent: parent,
|
||||
fullPath: {
|
||||
identifier: parent.fullPath.identifier,
|
||||
reactive: parent.fullPath.reactive,
|
||||
path: parent.fullPath.path.concat(entry),
|
||||
},
|
||||
hasOptional: parent.hasOptional || entry.optional,
|
||||
@@ -293,7 +304,7 @@ class PropertyPathRegistry {
|
||||
* so all subpaths of a PropertyLoad should already exist
|
||||
* (e.g. a.b is added before a.b.c),
|
||||
*/
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier);
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
|
||||
if (n.path.length === 0) {
|
||||
return currNode;
|
||||
}
|
||||
@@ -315,10 +326,11 @@ function getMaybeNonNullInInstruction(
|
||||
instr: InstructionValue,
|
||||
context: CollectHoistablePropertyLoadsContext,
|
||||
): PropertyPathNode | null {
|
||||
let path = null;
|
||||
let path: ReactiveScopeDependency | null = null;
|
||||
if (instr.kind === 'PropertyLoad') {
|
||||
path = context.temporaries.get(instr.object.identifier.id) ?? {
|
||||
identifier: instr.object.identifier,
|
||||
reactive: instr.object.reactive,
|
||||
path: [],
|
||||
};
|
||||
} else if (instr.kind === 'Destructure') {
|
||||
@@ -381,7 +393,7 @@ function collectNonNullsInBlocks(
|
||||
) {
|
||||
const identifier = fn.params[0].identifier;
|
||||
knownNonNullIdentifiers.add(
|
||||
context.registry.getOrCreateIdentifier(identifier),
|
||||
context.registry.getOrCreateIdentifier(identifier, true),
|
||||
);
|
||||
}
|
||||
const nodes = new Map<
|
||||
@@ -616,9 +628,11 @@ function reduceMaybeOptionalChains(
|
||||
changed = false;
|
||||
|
||||
for (const original of optionalChainNodes) {
|
||||
let {identifier, path: origPath} = original.fullPath;
|
||||
let currNode: PropertyPathNode =
|
||||
registry.getOrCreateIdentifier(identifier);
|
||||
let {identifier, path: origPath, reactive} = original.fullPath;
|
||||
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
|
||||
identifier,
|
||||
reactive,
|
||||
);
|
||||
for (let i = 0; i < origPath.length; i++) {
|
||||
const entry = origPath[i];
|
||||
// If the base is known to be non-null, replace with a non-optional load
|
||||
|
||||
@@ -290,6 +290,7 @@ function traverseOptionalBlock(
|
||||
);
|
||||
baseObject = {
|
||||
identifier: maybeTest.instructions[0].value.place.identifier,
|
||||
reactive: maybeTest.instructions[0].value.place.reactive,
|
||||
path,
|
||||
};
|
||||
test = maybeTest.terminal;
|
||||
@@ -391,6 +392,7 @@ function traverseOptionalBlock(
|
||||
);
|
||||
const load = {
|
||||
identifier: baseObject.identifier,
|
||||
reactive: baseObject.reactive,
|
||||
path: [
|
||||
...baseObject.path,
|
||||
{
|
||||
|
||||
@@ -25,8 +25,9 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* `identifier.path`, or `identifier?.path` is in this map, it is safe to
|
||||
* evaluate (non-optional) PropertyLoads from.
|
||||
*/
|
||||
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
|
||||
#deps: Map<Identifier, DependencyNode> = new Map();
|
||||
#hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
|
||||
new Map();
|
||||
#deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
|
||||
|
||||
/**
|
||||
* @param hoistableObjects a set of paths from which we can safely evaluate
|
||||
@@ -35,9 +36,10 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* duplicates when traversing the CFG.
|
||||
*/
|
||||
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
|
||||
for (const {path, identifier} of hoistableObjects) {
|
||||
for (const {path, identifier, reactive} of hoistableObjects) {
|
||||
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#hoistableObjects,
|
||||
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
|
||||
);
|
||||
@@ -70,7 +72,8 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
|
||||
static #getOrCreateRoot<T extends string>(
|
||||
identifier: Identifier,
|
||||
roots: Map<Identifier, TreeNode<T>>,
|
||||
reactive: boolean,
|
||||
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
|
||||
defaultAccessType: T,
|
||||
): TreeNode<T> {
|
||||
// roots can always be accessed unconditionally in JS
|
||||
@@ -79,9 +82,16 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
if (rootNode === undefined) {
|
||||
rootNode = {
|
||||
properties: new Map(),
|
||||
reactive,
|
||||
accessType: defaultAccessType,
|
||||
};
|
||||
roots.set(identifier, rootNode);
|
||||
} else {
|
||||
CompilerError.invariant(reactive === rootNode.reactive, {
|
||||
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
|
||||
description: `Identifier ${printIdentifier(identifier)}`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
@@ -92,9 +102,10 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* safe-to-evaluate subpath
|
||||
*/
|
||||
addDependency(dep: ReactiveScopeDependency): void {
|
||||
const {identifier, path} = dep;
|
||||
const {identifier, reactive, path} = dep;
|
||||
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#deps,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
);
|
||||
@@ -172,7 +183,13 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
|
||||
const results = new Set<ReactiveScopeDependency>();
|
||||
for (const [rootId, rootNode] of this.#deps.entries()) {
|
||||
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
|
||||
collectMinimalDependenciesInSubtree(
|
||||
rootNode,
|
||||
rootNode.reactive,
|
||||
rootId,
|
||||
[],
|
||||
results,
|
||||
);
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -294,25 +311,24 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
|
||||
type DependencyNode = TreeNode<PropertyAccessType>;
|
||||
|
||||
/**
|
||||
* TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
|
||||
* longer have conditionally accessed nodes, we can simplify
|
||||
*
|
||||
* Recursively calculates minimal dependencies in a subtree.
|
||||
* @param node DependencyNode representing a dependency subtree.
|
||||
* @returns a minimal list of dependencies in this subtree.
|
||||
*/
|
||||
function collectMinimalDependenciesInSubtree(
|
||||
node: DependencyNode,
|
||||
reactive: boolean,
|
||||
rootIdentifier: Identifier,
|
||||
path: Array<DependencyPathEntry>,
|
||||
results: Set<ReactiveScopeDependency>,
|
||||
): void {
|
||||
if (isDependency(node.accessType)) {
|
||||
results.add({identifier: rootIdentifier, path});
|
||||
results.add({identifier: rootIdentifier, reactive, path});
|
||||
} else {
|
||||
for (const [childName, childNode] of node.properties) {
|
||||
collectMinimalDependenciesInSubtree(
|
||||
childNode,
|
||||
reactive,
|
||||
rootIdentifier,
|
||||
[
|
||||
...path,
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
ShapeRegistry,
|
||||
addHook,
|
||||
} from './ObjectShape';
|
||||
import {Scope as BabelScope} from '@babel/traverse';
|
||||
import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
@@ -675,6 +675,7 @@ export class Environment {
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
parentFunction: NodePath<t.Function>;
|
||||
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
@@ -682,6 +683,7 @@ export class Environment {
|
||||
compilerMode: CompilerMode,
|
||||
config: EnvironmentConfig,
|
||||
contextIdentifiers: Set<t.Identifier>,
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
@@ -740,6 +742,7 @@ export class Environment {
|
||||
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
|
||||
}
|
||||
|
||||
this.parentFunction = parentFunction;
|
||||
this.#contextIdentifiers = contextIdentifiers;
|
||||
this.#hoistedIdentifiers = new Set();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
BuiltInSetId,
|
||||
BuiltInUseActionStateId,
|
||||
BuiltInUseContextHookId,
|
||||
BuiltInUseEffectEventId,
|
||||
BuiltInUseEffectHookId,
|
||||
BuiltInUseInsertionEffectHookId,
|
||||
BuiltInUseLayoutEffectHookId,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
BuiltInUseTransitionId,
|
||||
BuiltInWeakMapId,
|
||||
BuiltInWeakSetId,
|
||||
BuiltinEffectEventId,
|
||||
ReanimatedSharedValueId,
|
||||
ShapeRegistry,
|
||||
addFunction,
|
||||
@@ -722,6 +724,27 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInFireId,
|
||||
),
|
||||
],
|
||||
[
|
||||
'useEffectEvent',
|
||||
addHook(
|
||||
DEFAULT_SHAPES,
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: {
|
||||
kind: 'Function',
|
||||
return: {kind: 'Poly'},
|
||||
shapeId: BuiltinEffectEventId,
|
||||
isConstructor: false,
|
||||
},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useEffectEvent',
|
||||
// Frozen because it should not mutate any locally-bound values
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
},
|
||||
BuiltInUseEffectEventId,
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
TYPED_GLOBALS.push(
|
||||
|
||||
@@ -1568,6 +1568,18 @@ export type DependencyPathEntry = {
|
||||
export type DependencyPath = Array<DependencyPathEntry>;
|
||||
export type ReactiveScopeDependency = {
|
||||
identifier: Identifier;
|
||||
/**
|
||||
* Reflects whether the base identifier is reactive. Note that some reactive
|
||||
* objects may have non-reactive properties, but we do not currently track
|
||||
* this.
|
||||
*
|
||||
* ```js
|
||||
* // Technically, result[0] is reactive and result[1] is not.
|
||||
* // Currently, both dependencies would be marked as reactive.
|
||||
* const result = useState();
|
||||
* ```
|
||||
*/
|
||||
reactive: boolean;
|
||||
path: DependencyPath;
|
||||
};
|
||||
|
||||
@@ -1773,6 +1785,13 @@ export function isFireFunctionType(id: Identifier): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isEffectEventFunctionType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' &&
|
||||
id.type.shapeId === 'BuiltInEffectEventFunction'
|
||||
);
|
||||
}
|
||||
|
||||
export function isStableType(id: Identifier): boolean {
|
||||
return (
|
||||
isSetStateType(id) ||
|
||||
|
||||
@@ -110,7 +110,6 @@ export default class HIRBuilder {
|
||||
#bindings: Bindings;
|
||||
#env: Environment;
|
||||
#exceptionHandlerStack: Array<BlockId> = [];
|
||||
parentFunction: NodePath<t.Function>;
|
||||
errors: CompilerError = new CompilerError();
|
||||
/**
|
||||
* Traversal context: counts the number of `fbt` tag parents
|
||||
@@ -136,16 +135,17 @@ export default class HIRBuilder {
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
bindings: Bindings | null = null,
|
||||
context: Array<t.Identifier> | null = null,
|
||||
options?: {
|
||||
bindings?: Bindings | null;
|
||||
context?: Array<t.Identifier>;
|
||||
entryBlockKind?: BlockKind;
|
||||
},
|
||||
) {
|
||||
this.#env = env;
|
||||
this.#bindings = bindings ?? new Map();
|
||||
this.parentFunction = parentFunction;
|
||||
this.#context = context ?? [];
|
||||
this.#bindings = options?.bindings ?? new Map();
|
||||
this.#context = options?.context ?? [];
|
||||
this.#entry = makeBlockId(env.nextBlockId);
|
||||
this.#current = newBlock(this.#entry, 'block');
|
||||
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
|
||||
}
|
||||
|
||||
currentBlockKind(): BlockKind {
|
||||
@@ -239,7 +239,7 @@ export default class HIRBuilder {
|
||||
|
||||
// Check if the binding is from module scope
|
||||
const outerBinding =
|
||||
this.parentFunction.scope.parent.getBinding(originalName);
|
||||
this.#env.parentFunction.scope.parent.getBinding(originalName);
|
||||
if (babelBinding === outerBinding) {
|
||||
const path = babelBinding.path;
|
||||
if (path.isImportDefaultSpecifier()) {
|
||||
@@ -293,7 +293,7 @@ export default class HIRBuilder {
|
||||
const binding = this.#resolveBabelBinding(path);
|
||||
if (binding) {
|
||||
// Check if the binding is from module scope, if so return null
|
||||
const outerBinding = this.parentFunction.scope.parent.getBinding(
|
||||
const outerBinding = this.#env.parentFunction.scope.parent.getBinding(
|
||||
path.node.name,
|
||||
);
|
||||
if (binding === outerBinding) {
|
||||
@@ -376,7 +376,7 @@ export default class HIRBuilder {
|
||||
}
|
||||
|
||||
// Terminate the current block w the given terminal, and start a new block
|
||||
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void {
|
||||
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {
|
||||
const {id: blockId, kind, instructions} = this.#current;
|
||||
this.#completed.set(blockId, {
|
||||
kind,
|
||||
@@ -390,6 +390,7 @@ export default class HIRBuilder {
|
||||
const nextId = this.#env.nextBlockId;
|
||||
this.#current = newBlock(nextId, nextBlockKind);
|
||||
}
|
||||
return blockId;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -746,6 +747,11 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
|
||||
* (eg bb2 then bb1), we ensure that they get reversed back to the correct order.
|
||||
*/
|
||||
const block = func.blocks.get(blockId)!;
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: '[HIRBuilder] Unexpected null block',
|
||||
description: `expected block ${blockId} to exist`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
|
||||
const fallthrough = terminalFallthrough(block.terminal);
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ export type HookKind =
|
||||
| 'useCallback'
|
||||
| 'useTransition'
|
||||
| 'useImperativeHandle'
|
||||
| 'useEffectEvent'
|
||||
| 'Custom';
|
||||
|
||||
/*
|
||||
@@ -226,6 +227,8 @@ export const BuiltInUseTransitionId = 'BuiltInUseTransition';
|
||||
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
|
||||
export const BuiltInFireId = 'BuiltInFire';
|
||||
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
|
||||
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
|
||||
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
|
||||
|
||||
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
|
||||
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
|
||||
@@ -948,6 +951,19 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
|
||||
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
|
||||
]);
|
||||
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.ConditionallyMutate,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
BuiltinEffectEventId,
|
||||
);
|
||||
|
||||
/**
|
||||
* MixedReadOnly =
|
||||
* | primitive
|
||||
|
||||
@@ -316,6 +316,7 @@ function collectTemporariesSidemapImpl(
|
||||
) {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: value.place.identifier,
|
||||
reactive: value.place.reactive,
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
@@ -369,11 +370,13 @@ function getProperty(
|
||||
if (resolvedDependency == null) {
|
||||
property = {
|
||||
identifier: object.identifier,
|
||||
reactive: object.reactive,
|
||||
path: [{property: propertyName, optional}],
|
||||
};
|
||||
} else {
|
||||
property = {
|
||||
identifier: resolvedDependency.identifier,
|
||||
reactive: resolvedDependency.reactive,
|
||||
path: [...resolvedDependency.path, {property: propertyName, optional}],
|
||||
};
|
||||
}
|
||||
@@ -532,6 +535,7 @@ export class DependencyCollectionContext {
|
||||
this.visitDependency(
|
||||
this.#temporaries.get(place.identifier.id) ?? {
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
},
|
||||
);
|
||||
@@ -596,6 +600,7 @@ export class DependencyCollectionContext {
|
||||
) {
|
||||
maybeDependency = {
|
||||
identifier: maybeDependency.identifier,
|
||||
reactive: maybeDependency.reactive,
|
||||
path: [],
|
||||
};
|
||||
}
|
||||
@@ -617,7 +622,11 @@ export class DependencyCollectionContext {
|
||||
identifier =>
|
||||
identifier.declarationId === place.identifier.declarationId,
|
||||
) &&
|
||||
this.#checkValidDependency({identifier: place.identifier, path: []})
|
||||
this.#checkValidDependency({
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
})
|
||||
) {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import {
|
||||
Place,
|
||||
ReactiveScopeDependency,
|
||||
Identifier,
|
||||
makeInstructionId,
|
||||
InstructionKind,
|
||||
GeneratedSource,
|
||||
BlockId,
|
||||
makeTemporaryIdentifier,
|
||||
Effect,
|
||||
GotoVariant,
|
||||
HIR,
|
||||
} from './HIR';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import HIRBuilder from './HIRBuilder';
|
||||
import {lowerValueToTemporary} from './BuildHIR';
|
||||
|
||||
type DependencyInstructions = {
|
||||
place: Place;
|
||||
value: HIR;
|
||||
exitBlockId: BlockId;
|
||||
};
|
||||
|
||||
export function buildDependencyInstructions(
|
||||
dep: ReactiveScopeDependency,
|
||||
env: Environment,
|
||||
): DependencyInstructions {
|
||||
const builder = new HIRBuilder(env, {
|
||||
entryBlockKind: 'value',
|
||||
});
|
||||
let dependencyValue: Identifier;
|
||||
if (dep.path.every(path => !path.optional)) {
|
||||
dependencyValue = writeNonOptionalDependency(dep, env, builder);
|
||||
} else {
|
||||
dependencyValue = writeOptionalDependency(dep, builder, null);
|
||||
}
|
||||
|
||||
const exitBlockId = builder.terminate(
|
||||
{
|
||||
kind: 'unsupported',
|
||||
loc: GeneratedSource,
|
||||
id: makeInstructionId(0),
|
||||
},
|
||||
null,
|
||||
);
|
||||
return {
|
||||
place: {
|
||||
kind: 'Identifier',
|
||||
identifier: dependencyValue,
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
value: builder.build(),
|
||||
exitBlockId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write instructions for a simple dependency (without optional chains)
|
||||
*/
|
||||
function writeNonOptionalDependency(
|
||||
dep: ReactiveScopeDependency,
|
||||
env: Environment,
|
||||
builder: HIRBuilder,
|
||||
): Identifier {
|
||||
const loc = dep.identifier.loc;
|
||||
let curr: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc);
|
||||
builder.push({
|
||||
lvalue: {
|
||||
identifier: curr,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {
|
||||
identifier: dep.identifier,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
});
|
||||
|
||||
/**
|
||||
* Iteratively build up dependency instructions by reading from the last written
|
||||
* instruction.
|
||||
*/
|
||||
for (const path of dep.path) {
|
||||
const next = makeTemporaryIdentifier(env.nextIdentifierId, loc);
|
||||
builder.push({
|
||||
lvalue: {
|
||||
identifier: next,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
value: {
|
||||
kind: 'PropertyLoad',
|
||||
object: {
|
||||
identifier: curr,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
property: path.property,
|
||||
loc,
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
});
|
||||
curr = next;
|
||||
}
|
||||
return curr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a dependency into optional blocks.
|
||||
*
|
||||
* e.g. `a.b?.c.d` is written to an optional block that tests `a.b` and
|
||||
* conditionally evaluates `c.d`.
|
||||
*/
|
||||
function writeOptionalDependency(
|
||||
dep: ReactiveScopeDependency,
|
||||
builder: HIRBuilder,
|
||||
parentAlternate: BlockId | null,
|
||||
): Identifier {
|
||||
const env = builder.environment;
|
||||
/**
|
||||
* Reserve an identifier which will be used to store the result of this
|
||||
* dependency.
|
||||
*/
|
||||
const dependencyValue: Place = {
|
||||
kind: 'Identifier',
|
||||
identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource),
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reserve a block which is the fallthrough (and transitive successor) of this
|
||||
* optional chain.
|
||||
*/
|
||||
const continuationBlock = builder.reserve(builder.currentBlockKind());
|
||||
let alternate;
|
||||
if (parentAlternate != null) {
|
||||
alternate = parentAlternate;
|
||||
} else {
|
||||
/**
|
||||
* If an outermost alternate block has not been reserved, write one
|
||||
*
|
||||
* $N = Primitive undefined
|
||||
* $M = StoreLocal $OptionalResult = $N
|
||||
* goto fallthrough
|
||||
*/
|
||||
alternate = builder.enter('value', () => {
|
||||
const temp = lowerValueToTemporary(builder, {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
|
||||
value: {...temp},
|
||||
type: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Reserve the consequent block, which is the successor of the test block
|
||||
const consequent = builder.reserve('value');
|
||||
|
||||
let testIdentifier: Identifier | null = null;
|
||||
const testBlock = builder.enter('value', () => {
|
||||
const testDependency = {
|
||||
...dep,
|
||||
path: dep.path.slice(0, dep.path.length - 1),
|
||||
};
|
||||
const firstOptional = dep.path.findIndex(path => path.optional);
|
||||
CompilerError.invariant(firstOptional !== -1, {
|
||||
reason:
|
||||
'[ScopeDependencyUtils] Internal invariant broken: expected optional path',
|
||||
loc: dep.identifier.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
if (firstOptional === dep.path.length - 1) {
|
||||
// Base case: the test block is simple
|
||||
testIdentifier = writeNonOptionalDependency(testDependency, env, builder);
|
||||
} else {
|
||||
// Otherwise, the test block is a nested optional chain
|
||||
testIdentifier = writeOptionalDependency(
|
||||
testDependency,
|
||||
builder,
|
||||
alternate,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'branch',
|
||||
test: {
|
||||
identifier: testIdentifier,
|
||||
effect: Effect.Freeze,
|
||||
kind: 'Identifier',
|
||||
loc: GeneratedSource,
|
||||
reactive: dep.reactive,
|
||||
},
|
||||
consequent: consequent.id,
|
||||
alternate,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
fallthrough: continuationBlock.id,
|
||||
};
|
||||
});
|
||||
|
||||
builder.enterReserved(consequent, () => {
|
||||
CompilerError.invariant(testIdentifier !== null, {
|
||||
reason: 'Satisfy type checker',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
|
||||
value: lowerValueToTemporary(builder, {
|
||||
kind: 'PropertyLoad',
|
||||
object: {
|
||||
identifier: testIdentifier,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
property: dep.path.at(-1)!.property,
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
type: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
});
|
||||
builder.terminateWithContinuation(
|
||||
{
|
||||
kind: 'optional',
|
||||
optional: dep.path.at(-1)!.optional,
|
||||
test: testBlock,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
continuationBlock,
|
||||
);
|
||||
|
||||
return dependencyValue.identifier;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
ArrayExpression,
|
||||
Effect,
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
@@ -29,6 +28,10 @@ import {
|
||||
isSetStateType,
|
||||
isFireFunctionType,
|
||||
makeScopeId,
|
||||
HIR,
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
isEffectEventFunctionType,
|
||||
} from '../HIR';
|
||||
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
|
||||
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
|
||||
@@ -38,13 +41,20 @@ import {
|
||||
createTemporaryPlace,
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
markPredecessors,
|
||||
reversePostorderBlocks,
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {
|
||||
collectTemporariesSidemap,
|
||||
DependencyCollectionContext,
|
||||
handleInstruction,
|
||||
} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {buildDependencyInstructions} from '../HIR/ScopeDependencyUtils';
|
||||
import {
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
terminalFallthrough,
|
||||
} from '../HIR/visitors';
|
||||
import {empty} from '../Utils/Stack';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
|
||||
@@ -53,7 +63,6 @@ import {getOrInsertWith} from '../Utils/utils';
|
||||
* a second argument to the useEffect call if no dependency array is provided.
|
||||
*/
|
||||
export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
let hasRewrite = false;
|
||||
const fnExpressions = new Map<
|
||||
IdentifierId,
|
||||
TInstruction<FunctionExpression>
|
||||
@@ -86,6 +95,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
* reactive(Identifier i) = Union_{reference of i}(reactive(reference))
|
||||
*/
|
||||
const reactiveIds = inferReactiveIdentifiers(fn);
|
||||
const rewriteBlocks: Array<BasicBlock> = [];
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
if (block.terminal.kind === 'scope') {
|
||||
@@ -101,7 +111,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
);
|
||||
}
|
||||
}
|
||||
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
|
||||
const rewriteInstrs: Array<SpliceInfo> = [];
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
@@ -165,7 +175,6 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
) {
|
||||
// We have a useEffect call with no deps array, so we need to infer the deps
|
||||
const effectDeps: Array<Place> = [];
|
||||
const newInstructions: Array<Instruction> = [];
|
||||
const deps: ArrayExpression = {
|
||||
kind: 'ArrayExpression',
|
||||
elements: effectDeps,
|
||||
@@ -196,24 +205,29 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
*/
|
||||
|
||||
const usedDeps = [];
|
||||
for (const dep of minimalDeps) {
|
||||
for (const maybeDep of minimalDeps) {
|
||||
if (
|
||||
((isUseRefType(dep.identifier) ||
|
||||
isSetStateType(dep.identifier)) &&
|
||||
!reactiveIds.has(dep.identifier.id)) ||
|
||||
isFireFunctionType(dep.identifier)
|
||||
((isUseRefType(maybeDep.identifier) ||
|
||||
isSetStateType(maybeDep.identifier)) &&
|
||||
!reactiveIds.has(maybeDep.identifier.id)) ||
|
||||
isFireFunctionType(maybeDep.identifier) ||
|
||||
isEffectEventFunctionType(maybeDep.identifier)
|
||||
) {
|
||||
// exclude non-reactive hook results, which will never be in a memo block
|
||||
continue;
|
||||
}
|
||||
|
||||
const {place, instructions} = writeDependencyToInstructions(
|
||||
const dep = truncateDepAtCurrent(maybeDep);
|
||||
const {place, value, exitBlockId} = buildDependencyInstructions(
|
||||
dep,
|
||||
reactiveIds.has(dep.identifier.id),
|
||||
fn.env,
|
||||
fnExpr.loc,
|
||||
);
|
||||
newInstructions.push(...instructions);
|
||||
rewriteInstrs.push({
|
||||
kind: 'block',
|
||||
location: instr.id,
|
||||
value,
|
||||
exitBlockId: exitBlockId,
|
||||
});
|
||||
effectDeps.push(place);
|
||||
usedDeps.push(dep);
|
||||
}
|
||||
@@ -234,27 +248,32 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
});
|
||||
}
|
||||
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
});
|
||||
|
||||
// Step 2: push the inferred deps array as an argument of the useEffect
|
||||
rewriteInstrs.push({
|
||||
kind: 'instr',
|
||||
location: instr.id,
|
||||
value: {
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
},
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
} else if (loadGlobals.has(value.args[0].identifier.id)) {
|
||||
// Global functions have no reactive dependencies, so we can insert an empty array
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
rewriteInstrs.push({
|
||||
kind: 'instr',
|
||||
location: instr.id,
|
||||
value: {
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
},
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
}
|
||||
} else if (
|
||||
@@ -285,85 +304,164 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rewriteInstrs.size > 0) {
|
||||
hasRewrite = true;
|
||||
const newInstrs = [];
|
||||
for (const instr of block.instructions) {
|
||||
const newInstr = rewriteInstrs.get(instr.id);
|
||||
if (newInstr != null) {
|
||||
newInstrs.push(...newInstr, instr);
|
||||
} else {
|
||||
newInstrs.push(instr);
|
||||
}
|
||||
}
|
||||
block.instructions = newInstrs;
|
||||
}
|
||||
rewriteSplices(block, rewriteInstrs, rewriteBlocks);
|
||||
}
|
||||
if (hasRewrite) {
|
||||
|
||||
if (rewriteBlocks.length > 0) {
|
||||
for (const block of rewriteBlocks) {
|
||||
fn.body.blocks.set(block.id, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixup the HIR to restore RPO, ensure correct predecessors, and renumber
|
||||
* instructions.
|
||||
*/
|
||||
reversePostorderBlocks(fn.body);
|
||||
markPredecessors(fn.body);
|
||||
// Renumber instructions and fix scope ranges
|
||||
markInstructionIds(fn.body);
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
|
||||
fn.env.hasInferredEffect = true;
|
||||
}
|
||||
}
|
||||
|
||||
function writeDependencyToInstructions(
|
||||
function truncateDepAtCurrent(
|
||||
dep: ReactiveScopeDependency,
|
||||
reactive: boolean,
|
||||
env: Environment,
|
||||
loc: SourceLocation,
|
||||
): {place: Place; instructions: Array<Instruction>} {
|
||||
const instructions: Array<Instruction> = [];
|
||||
let currValue = createTemporaryPlace(env, GeneratedSource);
|
||||
currValue.reactive = reactive;
|
||||
instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...currValue, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {
|
||||
kind: 'Identifier',
|
||||
identifier: dep.identifier,
|
||||
effect: Effect.Capture,
|
||||
reactive,
|
||||
loc: loc,
|
||||
},
|
||||
loc: loc,
|
||||
},
|
||||
});
|
||||
for (const path of dep.path) {
|
||||
if (path.optional) {
|
||||
/**
|
||||
* TODO: instead of truncating optional paths, reuse
|
||||
* instructions from hoisted dependencies block(s)
|
||||
*/
|
||||
break;
|
||||
}
|
||||
if (path.property === 'current') {
|
||||
/*
|
||||
* Prune ref.current accesses. This may over-capture for non-ref values with
|
||||
* a current property, but that's fine.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
const nextValue = createTemporaryPlace(env, GeneratedSource);
|
||||
nextValue.reactive = reactive;
|
||||
instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...nextValue, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'PropertyLoad',
|
||||
object: {...currValue, effect: Effect.Capture},
|
||||
property: path.property,
|
||||
loc: loc,
|
||||
},
|
||||
});
|
||||
currValue = nextValue;
|
||||
): ReactiveScopeDependency {
|
||||
const idx = dep.path.findIndex(path => path.property === 'current');
|
||||
if (idx === -1) {
|
||||
return dep;
|
||||
} else {
|
||||
return {...dep, path: dep.path.slice(0, idx)};
|
||||
}
|
||||
currValue.effect = Effect.Freeze;
|
||||
return {place: currValue, instructions};
|
||||
}
|
||||
|
||||
type SpliceInfo =
|
||||
| {kind: 'instr'; location: InstructionId; value: Instruction}
|
||||
| {
|
||||
kind: 'block';
|
||||
location: InstructionId;
|
||||
value: HIR;
|
||||
exitBlockId: BlockId;
|
||||
};
|
||||
|
||||
function rewriteSplices(
|
||||
originalBlock: BasicBlock,
|
||||
splices: Array<SpliceInfo>,
|
||||
rewriteBlocks: Array<BasicBlock>,
|
||||
): void {
|
||||
if (splices.length === 0) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Splice instructions or value blocks into the original block.
|
||||
* --- original block ---
|
||||
* bb_original
|
||||
* instr1
|
||||
* ...
|
||||
* instr2 <-- splice location
|
||||
* instr3
|
||||
* ...
|
||||
* <original terminal>
|
||||
*
|
||||
* If there is more than one block in the splice, this means that we're
|
||||
* splicing in a set of value-blocks of the following structure:
|
||||
* --- blocks we're splicing in ---
|
||||
* bb_entry:
|
||||
* instrEntry
|
||||
* ...
|
||||
* <splice terminal> fallthrough=bb_exit
|
||||
*
|
||||
* bb1(value):
|
||||
* ...
|
||||
*
|
||||
* bb_exit:
|
||||
* instrExit
|
||||
* ...
|
||||
* <synthetic terminal>
|
||||
*
|
||||
*
|
||||
* --- rewritten blocks ---
|
||||
* bb_original
|
||||
* instr1
|
||||
* ... (original instructions)
|
||||
* instr2
|
||||
* instrEntry
|
||||
* ... (spliced instructions)
|
||||
* <splice terminal> fallthrough=bb_exit
|
||||
*
|
||||
* bb1(value):
|
||||
* ...
|
||||
*
|
||||
* bb_exit:
|
||||
* instrExit
|
||||
* ... (spliced instructions)
|
||||
* instr3
|
||||
* ... (original instructions)
|
||||
* <original terminal>
|
||||
*/
|
||||
const originalInstrs = originalBlock.instructions;
|
||||
let currBlock: BasicBlock = {...originalBlock, instructions: []};
|
||||
rewriteBlocks.push(currBlock);
|
||||
|
||||
let cursor = 0;
|
||||
for (const rewrite of splices) {
|
||||
while (originalInstrs[cursor].id < rewrite.location) {
|
||||
CompilerError.invariant(
|
||||
originalInstrs[cursor].id < originalInstrs[cursor + 1].id,
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
},
|
||||
);
|
||||
currBlock.instructions.push(originalInstrs[cursor]);
|
||||
cursor++;
|
||||
}
|
||||
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: splice location not found',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
});
|
||||
|
||||
if (rewrite.kind === 'instr') {
|
||||
currBlock.instructions.push(rewrite.value);
|
||||
} else {
|
||||
const {entry, blocks} = rewrite.value;
|
||||
const entryBlock = blocks.get(entry)!;
|
||||
// splice in all instructions from the entry block
|
||||
currBlock.instructions.push(...entryBlock.instructions);
|
||||
if (blocks.size > 1) {
|
||||
/**
|
||||
* We're splicing in a set of value-blocks, which means we need
|
||||
* to push new blocks and update terminals.
|
||||
*/
|
||||
CompilerError.invariant(
|
||||
terminalFallthrough(entryBlock.terminal) === rewrite.exitBlockId,
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
|
||||
loc: entryBlock.terminal.loc,
|
||||
},
|
||||
);
|
||||
const originalTerminal = currBlock.terminal;
|
||||
currBlock.terminal = entryBlock.terminal;
|
||||
|
||||
for (const [id, block] of blocks) {
|
||||
if (id === entry) {
|
||||
continue;
|
||||
}
|
||||
if (id === rewrite.exitBlockId) {
|
||||
block.terminal = originalTerminal;
|
||||
currBlock = block;
|
||||
}
|
||||
rewriteBlocks.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
currBlock.instructions.push(...originalInstrs.slice(cursor));
|
||||
}
|
||||
|
||||
function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
@@ -292,7 +293,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
let hasReactiveInput = false;
|
||||
/*
|
||||
* NOTE: we want to mark all operands as reactive or not, so we
|
||||
* avoid short-circuting here
|
||||
* avoid short-circuiting here
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const reactive = reactiveIdentifiers.isReactive(operand);
|
||||
@@ -375,6 +376,41 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
} while (reactiveIdentifiers.snapshot());
|
||||
|
||||
function propagateReactivityToInnerFunctions(
|
||||
fn: HIRFunction,
|
||||
isOutermost: boolean,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (!isOutermost) {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
reactiveIdentifiers.isReactive(operand);
|
||||
}
|
||||
}
|
||||
if (
|
||||
instr.value.kind === 'ObjectMethod' ||
|
||||
instr.value.kind === 'FunctionExpression'
|
||||
) {
|
||||
propagateReactivityToInnerFunctions(
|
||||
instr.value.loweredFunc.func,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!isOutermost) {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
reactiveIdentifiers.isReactive(operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate reactivity for inner functions, as we eventually hoist and dedupe
|
||||
* dependency instructions for scopes.
|
||||
*/
|
||||
propagateReactivityToInnerFunctions(fn, true);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
InstructionKind,
|
||||
LabelTerminal,
|
||||
Place,
|
||||
isStatementBlockKind,
|
||||
makeInstructionId,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
@@ -90,100 +91,106 @@ export function inlineImmediatelyInvokedFunctionExpressions(
|
||||
*/
|
||||
const queue = Array.from(fn.body.blocks.values());
|
||||
queue: for (const block of queue) {
|
||||
for (let ii = 0; ii < block.instructions.length; ii++) {
|
||||
const instr = block.instructions[ii]!;
|
||||
switch (instr.value.kind) {
|
||||
case 'FunctionExpression': {
|
||||
if (instr.lvalue.identifier.name === null) {
|
||||
functions.set(instr.lvalue.identifier.id, instr.value);
|
||||
/*
|
||||
* We can't handle labels inside expressions yet, so we don't inline IIFEs if they are in an
|
||||
* expression block.
|
||||
*/
|
||||
if (isStatementBlockKind(block.kind)) {
|
||||
for (let ii = 0; ii < block.instructions.length; ii++) {
|
||||
const instr = block.instructions[ii]!;
|
||||
switch (instr.value.kind) {
|
||||
case 'FunctionExpression': {
|
||||
if (instr.lvalue.identifier.name === null) {
|
||||
functions.set(instr.lvalue.identifier.id, instr.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CallExpression': {
|
||||
if (instr.value.args.length !== 0) {
|
||||
// We don't support inlining when there are arguments
|
||||
continue;
|
||||
case 'CallExpression': {
|
||||
if (instr.value.args.length !== 0) {
|
||||
// We don't support inlining when there are arguments
|
||||
continue;
|
||||
}
|
||||
const body = functions.get(instr.value.callee.identifier.id);
|
||||
if (body === undefined) {
|
||||
// Not invoking a local function expression, can't inline
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
body.loweredFunc.func.params.length > 0 ||
|
||||
body.loweredFunc.func.async ||
|
||||
body.loweredFunc.func.generator
|
||||
) {
|
||||
// Can't inline functions with params, or async/generator functions
|
||||
continue;
|
||||
}
|
||||
|
||||
// We know this function is used for an IIFE and can prune it later
|
||||
inlinedFunctions.add(instr.value.callee.identifier.id);
|
||||
|
||||
// Create a new block which will contain code following the IIFE call
|
||||
const continuationBlockId = fn.env.nextBlockId;
|
||||
const continuationBlock: BasicBlock = {
|
||||
id: continuationBlockId,
|
||||
instructions: block.instructions.slice(ii + 1),
|
||||
kind: block.kind,
|
||||
phis: new Set(),
|
||||
preds: new Set(),
|
||||
terminal: block.terminal,
|
||||
};
|
||||
fn.body.blocks.set(continuationBlockId, continuationBlock);
|
||||
|
||||
/*
|
||||
* Trim the original block to contain instructions up to (but not including)
|
||||
* the IIFE
|
||||
*/
|
||||
block.instructions.length = ii;
|
||||
|
||||
/*
|
||||
* To account for complex control flow within the lambda, we treat the lambda
|
||||
* as if it were a single labeled statement, and replace all returns with gotos
|
||||
* to the label fallthrough.
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block: body.loweredFunc.func.body.entry,
|
||||
id: makeInstructionId(0),
|
||||
kind: 'label',
|
||||
fallthrough: continuationBlockId,
|
||||
loc: block.terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
|
||||
// We store the result in the IIFE temporary
|
||||
const result = instr.lvalue;
|
||||
|
||||
// Declare the IIFE temporary
|
||||
declareTemporary(fn.env, block, result);
|
||||
|
||||
// Promote the temporary with a name as we require this to persist
|
||||
promoteTemporary(result.identifier);
|
||||
|
||||
/*
|
||||
* Rewrite blocks from the lambda to replace any `return` with a
|
||||
* store to the result and `goto` the continuation block
|
||||
*/
|
||||
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
||||
block.preds.clear();
|
||||
rewriteBlock(fn.env, block, continuationBlockId, result);
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure we visit the continuation block, since there may have been
|
||||
* sequential IIFEs that need to be visited.
|
||||
*/
|
||||
queue.push(continuationBlock);
|
||||
continue queue;
|
||||
}
|
||||
const body = functions.get(instr.value.callee.identifier.id);
|
||||
if (body === undefined) {
|
||||
// Not invoking a local function expression, can't inline
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
body.loweredFunc.func.params.length > 0 ||
|
||||
body.loweredFunc.func.async ||
|
||||
body.loweredFunc.func.generator
|
||||
) {
|
||||
// Can't inline functions with params, or async/generator functions
|
||||
continue;
|
||||
}
|
||||
|
||||
// We know this function is used for an IIFE and can prune it later
|
||||
inlinedFunctions.add(instr.value.callee.identifier.id);
|
||||
|
||||
// Create a new block which will contain code following the IIFE call
|
||||
const continuationBlockId = fn.env.nextBlockId;
|
||||
const continuationBlock: BasicBlock = {
|
||||
id: continuationBlockId,
|
||||
instructions: block.instructions.slice(ii + 1),
|
||||
kind: block.kind,
|
||||
phis: new Set(),
|
||||
preds: new Set(),
|
||||
terminal: block.terminal,
|
||||
};
|
||||
fn.body.blocks.set(continuationBlockId, continuationBlock);
|
||||
|
||||
/*
|
||||
* Trim the original block to contain instructions up to (but not including)
|
||||
* the IIFE
|
||||
*/
|
||||
block.instructions.length = ii;
|
||||
|
||||
/*
|
||||
* To account for complex control flow within the lambda, we treat the lambda
|
||||
* as if it were a single labeled statement, and replace all returns with gotos
|
||||
* to the label fallthrough.
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block: body.loweredFunc.func.body.entry,
|
||||
id: makeInstructionId(0),
|
||||
kind: 'label',
|
||||
fallthrough: continuationBlockId,
|
||||
loc: block.terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
|
||||
// We store the result in the IIFE temporary
|
||||
const result = instr.lvalue;
|
||||
|
||||
// Declare the IIFE temporary
|
||||
declareTemporary(fn.env, block, result);
|
||||
|
||||
// Promote the temporary with a name as we require this to persist
|
||||
promoteTemporary(result.identifier);
|
||||
|
||||
/*
|
||||
* Rewrite blocks from the lambda to replace any `return` with a
|
||||
* store to the result and `goto` the continuation block
|
||||
*/
|
||||
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
||||
block.preds.clear();
|
||||
rewriteBlock(fn.env, block, continuationBlockId, result);
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure we visit the continuation block, since there may have been
|
||||
* sequential IIFEs that need to be visited.
|
||||
*/
|
||||
queue.push(continuationBlock);
|
||||
continue queue;
|
||||
}
|
||||
default: {
|
||||
for (const place of eachInstructionValueOperand(instr.value)) {
|
||||
// Any other use of a function expression means it isn't an IIFE
|
||||
functions.delete(place.identifier.id);
|
||||
default: {
|
||||
for (const place of eachInstructionValueOperand(instr.value)) {
|
||||
// Any other use of a function expression means it isn't an IIFE
|
||||
functions.delete(place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1726,7 +1726,7 @@ function codegenInstructionValue(
|
||||
}
|
||||
case 'UnaryExpression': {
|
||||
value = t.unaryExpression(
|
||||
instrValue.operator as 'throw', // todo
|
||||
instrValue.operator,
|
||||
codegenPlaceToExpression(cx, instrValue.value),
|
||||
);
|
||||
break;
|
||||
@@ -2582,7 +2582,16 @@ function codegenValue(
|
||||
value: boolean | number | string | null | undefined,
|
||||
): t.Expression {
|
||||
if (typeof value === 'number') {
|
||||
return t.numericLiteral(value);
|
||||
if (value < 0) {
|
||||
/**
|
||||
* Babel's code generator produces invalid JS for negative numbers when
|
||||
* run with { compact: true }.
|
||||
* See repro https://codesandbox.io/p/devbox/5d47fr
|
||||
*/
|
||||
return t.unaryExpression('-', t.numericLiteral(-value), false);
|
||||
} else {
|
||||
return t.numericLiteral(value);
|
||||
}
|
||||
} else if (typeof value === 'boolean') {
|
||||
return t.booleanLiteral(value);
|
||||
} else if (typeof value === 'string') {
|
||||
|
||||
@@ -456,6 +456,7 @@ function canMergeScopes(
|
||||
new Set(
|
||||
[...current.scope.declarations.values()].map(declaration => ({
|
||||
identifier: declaration.identifier,
|
||||
reactive: true,
|
||||
path: [],
|
||||
})),
|
||||
),
|
||||
|
||||
@@ -93,6 +93,21 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function* splitPragma(
|
||||
pragma: string,
|
||||
): Generator<{key: string; value: string | null}> {
|
||||
for (const entry of pragma.split('@')) {
|
||||
const keyVal = entry.trim();
|
||||
const valIdx = keyVal.indexOf(':');
|
||||
if (valIdx === -1) {
|
||||
yield {key: keyVal.split(' ', 1)[0], value: null};
|
||||
} else {
|
||||
yield {key: keyVal.slice(0, valIdx), value: keyVal.slice(valIdx + 1)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For snap test fixtures and playground only.
|
||||
*/
|
||||
@@ -101,19 +116,11 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
): EnvironmentConfig {
|
||||
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
|
||||
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
const keyVal = token.slice(1);
|
||||
const valIdx = keyVal.indexOf(':');
|
||||
const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx);
|
||||
const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1);
|
||||
const isSet = val === undefined || val === 'true';
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isSet = val == null || val === 'true';
|
||||
if (isSet && key in testComplexConfigDefaults) {
|
||||
maybeConfig[key] = testComplexConfigDefaults[key];
|
||||
} else if (isSet) {
|
||||
@@ -176,18 +183,11 @@ export function parseConfigPragmaForTests(
|
||||
compilationMode: defaults.compilationMode,
|
||||
environment,
|
||||
};
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
const keyVal = token.slice(1);
|
||||
const idx = keyVal.indexOf(':');
|
||||
const key = idx === -1 ? keyVal : keyVal.slice(0, idx);
|
||||
const val = idx === -1 ? undefined : keyVal.slice(idx + 1);
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (!hasOwnProperty(defaultOptions, key)) {
|
||||
continue;
|
||||
}
|
||||
const isSet = val === undefined || val === 'true';
|
||||
const isSet = val == null || val === 'true';
|
||||
if (isSet && key in testComplexPluginOptionDefaults) {
|
||||
options[key] = testComplexPluginOptionDefaults[key];
|
||||
} else if (isSet) {
|
||||
|
||||
@@ -452,7 +452,7 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
|
||||
reason:
|
||||
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
|
||||
loc: callee.loc,
|
||||
description: `Cannot call ${hookKind} within a function component`,
|
||||
description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ export {validateContextVariableLValues} from './ValidateContextVariableLValues';
|
||||
export {validateHooksUsage} from './ValidateHooksUsage';
|
||||
export {validateMemoizedEffectDependencies} from './ValidateMemoizedEffectDependencies';
|
||||
export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
|
||||
export {validateNoRefAccessInRender} from './ValidateNoRefAccesInRender';
|
||||
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
|
||||
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
|
||||
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
|
||||
export {validateUseMemo} from './ValidateUseMemo';
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Repro(props) {
|
||||
const MY_CONST = -2;
|
||||
return <Stringify>{props.arg - MY_CONST}</Stringify>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Repro,
|
||||
params: [
|
||||
{
|
||||
arg: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Repro(props) {
|
||||
const $ = _c(2);
|
||||
|
||||
const t0 = props.arg - -2;
|
||||
let t1;
|
||||
if ($[0] !== t0) {
|
||||
t1 = <Stringify>{t0}</Stringify>;
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Repro,
|
||||
params: [
|
||||
{
|
||||
arg: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"children":5}</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Repro(props) {
|
||||
const MY_CONST = -2;
|
||||
return <Stringify>{props.arg - MY_CONST}</Stringify>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Repro,
|
||||
params: [
|
||||
{
|
||||
arg: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
/**
|
||||
* The postfix increment operator should return the value before incrementing.
|
||||
* ```js
|
||||
* const id = count.current; // 0
|
||||
* count.current = count.current + 1; // 1
|
||||
* return id;
|
||||
* ```
|
||||
* The bug is that we currently increment the value before the expression is evaluated.
|
||||
* This bug does not trigger when the incremented value is a plain primitive.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 0','count = 1']
|
||||
* Forget:
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 1','count = 1']
|
||||
*/
|
||||
function useFoo() {
|
||||
const count = useRef(0);
|
||||
const updateCountPostfix = () => {
|
||||
const id = count.current++;
|
||||
return id;
|
||||
};
|
||||
const updateCountPrefix = () => {
|
||||
const id = ++count.current;
|
||||
return id;
|
||||
};
|
||||
useEffect(() => {
|
||||
const id = updateCountPostfix();
|
||||
console.log(`id = ${id}`);
|
||||
console.log(`count = ${count.current}`);
|
||||
}, []);
|
||||
return {count, updateCountPostfix, updateCountPrefix};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* The postfix increment operator should return the value before incrementing.
|
||||
* ```js
|
||||
* const id = count.current; // 0
|
||||
* count.current = count.current + 1; // 1
|
||||
* return id;
|
||||
* ```
|
||||
* The bug is that we currently increment the value before the expression is evaluated.
|
||||
* This bug does not trigger when the incremented value is a plain primitive.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 0','count = 1']
|
||||
* Forget:
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 1','count = 1']
|
||||
*/
|
||||
function useFoo() {
|
||||
const $ = _c(5);
|
||||
const count = useRef(0);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
count.current = count.current + 1;
|
||||
const id = count.current;
|
||||
return id;
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const updateCountPostfix = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
const id_0 = (count.current = count.current + 1);
|
||||
return id_0;
|
||||
};
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const updateCountPrefix = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
const id_1 = updateCountPostfix();
|
||||
console.log(`id = ${id_1}`);
|
||||
console.log(`count = ${count.current}`);
|
||||
};
|
||||
t3 = [];
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = { count, updateCountPostfix, updateCountPrefix };
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
/**
|
||||
* The postfix increment operator should return the value before incrementing.
|
||||
* ```js
|
||||
* const id = count.current; // 0
|
||||
* count.current = count.current + 1; // 1
|
||||
* return id;
|
||||
* ```
|
||||
* The bug is that we currently increment the value before the expression is evaluated.
|
||||
* This bug does not trigger when the incremented value is a plain primitive.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 0','count = 1']
|
||||
* Forget:
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 1','count = 1']
|
||||
*/
|
||||
function useFoo() {
|
||||
const count = useRef(0);
|
||||
const updateCountPostfix = () => {
|
||||
const id = count.current++;
|
||||
return id;
|
||||
};
|
||||
const updateCountPrefix = () => {
|
||||
const id = ++count.current;
|
||||
return id;
|
||||
};
|
||||
useEffect(() => {
|
||||
const id = updateCountPostfix();
|
||||
console.log(`id = ${id}`);
|
||||
console.log(`count = ${count.current}`);
|
||||
}, []);
|
||||
return {count, updateCountPostfix, updateCountPrefix};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @customOptOutDirectives:["use todo memo"]
|
||||
function Component() {
|
||||
'use todo memo';
|
||||
return <div>hello world!</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @customOptOutDirectives:["use todo memo"]
|
||||
function Component() {
|
||||
"use todo memo";
|
||||
return <div>hello world!</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>hello world!</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
// @customOptOutDirectives:["use todo memo"]
|
||||
function Component() {
|
||||
'use todo memo';
|
||||
return <div>hello world!</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
return (
|
||||
useMemo(() => {
|
||||
return [props.value];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | function Component(props) {
|
||||
2 | return (
|
||||
> 3 | useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 4 | return [props.value];
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 5 | }) || []
|
||||
| ^^^^^^^^^^^^^ Todo: Support labeled statements combined with value blocks (conditional, logical, optional chaining, etc) (3:5)
|
||||
6 | );
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
function Component(props) {
|
||||
return (
|
||||
useMemo(() => {
|
||||
return [props.value];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation"
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { getTrue } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation"
|
||||
const Foo = getTrue()
|
||||
? function Foo() {
|
||||
"use memo if(getTrue)";
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div>hello world</div>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
: function Foo() {
|
||||
"use memo if(getTrue)";
|
||||
return <div>hello world</div>;
|
||||
};
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>hello world</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation"
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
function Foo({value}) {
|
||||
'use memo if(getTrue)';
|
||||
|
||||
const initialValue = useMemo(() => identity(value), []);
|
||||
return (
|
||||
<>
|
||||
<div>initial value {initialValue}</div>
|
||||
<div>current value {value}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{value: 1}],
|
||||
sequentialRenders: [{value: 1}, {value: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Foo({ value }) {
|
||||
"use memo if(getTrue)";
|
||||
|
||||
const initialValue = useMemo(() => identity(value), []);
|
||||
return (
|
||||
<>
|
||||
<div>initial value {initialValue}</div>
|
||||
<div>current value {value}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ value: 1 }],
|
||||
sequentialRenders: [{ value: 1 }, { value: 2 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":206},"end":{"line":16,"column":1,"index":433},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"reason":"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected","description":"The inferred dependency was `value`, but the source dependencies were []. Inferred dependency not present in source","severity":"CannotPreserveMemoization","suggestions":null,"loc":{"start":{"line":9,"column":31,"index":288},"end":{"line":9,"column":52,"index":309},"filename":"dynamic-gating-bailout-nopanic.ts"}}}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>initial value 1</div><div>current value 1</div>
|
||||
<div>initial value 1</div><div>current value 2</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
function Foo({value}) {
|
||||
'use memo if(getTrue)';
|
||||
|
||||
const initialValue = useMemo(() => identity(value), []);
|
||||
return (
|
||||
<>
|
||||
<div>initial value {initialValue}</div>
|
||||
<div>current value {value}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{value: 1}],
|
||||
sequentialRenders: [{value: 1}, {value: 2}],
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"}
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getFalse)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { getFalse } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"}
|
||||
const Foo = getFalse()
|
||||
? function Foo() {
|
||||
"use memo if(getFalse)";
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div>hello world</div>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
: function Foo() {
|
||||
"use memo if(getFalse)";
|
||||
return <div>hello world</div>;
|
||||
};
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>hello world</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"}
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getFalse)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"}
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { getTrue } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"}
|
||||
const Foo = getTrue()
|
||||
? function Foo() {
|
||||
"use memo if(getTrue)";
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div>hello world</div>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
: function Foo() {
|
||||
"use memo if(getTrue)";
|
||||
return <div>hello world</div>;
|
||||
};
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>hello world</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"}
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none"
|
||||
|
||||
function Foo() {
|
||||
'use memo if(true)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none"
|
||||
|
||||
function Foo() {
|
||||
"use memo if(true)";
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>hello world</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none"
|
||||
|
||||
function Foo() {
|
||||
'use memo if(true)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
'use memo if(getFalse)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly
|
||||
|
||||
function Foo() {
|
||||
"use memo if(getTrue)";
|
||||
"use memo if(getFalse)";
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","fnLoc":{"start":{"line":3,"column":0,"index":86},"end":{"line":7,"column":1,"index":190},"filename":"dynamic-gating-invalid-multiple.ts"},"detail":{"reason":"Multiple dynamic gating directives found","description":"Expected a single directive but found [use memo if(getTrue), use memo if(getFalse)]","severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":4,"column":2,"index":105},"end":{"line":4,"column":25,"index":128},"filename":"dynamic-gating-invalid-multiple.ts"}}}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>hello world</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
'use memo if(getFalse)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @noEmit
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @noEmit
|
||||
|
||||
function Foo() {
|
||||
"use memo if(getTrue)";
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>hello world</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"} @noEmit
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function ReactiveVariable({propVal}) {
|
||||
'use memo if(invalid identifier)';
|
||||
const arr = [propVal];
|
||||
useEffect(() => print(arr));
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ReactiveVariable,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
6 | 'use memo if(invalid identifier)';
|
||||
7 | const arr = [propVal];
|
||||
> 8 | useEffect(() => print(arr));
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (8:8)
|
||||
9 | }
|
||||
10 |
|
||||
11 | export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function ReactiveVariable({propVal}) {
|
||||
'use memo if(invalid identifier)';
|
||||
const arr = [propVal];
|
||||
useEffect(() => print(arr));
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ReactiveVariable,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"}
|
||||
|
||||
function Foo() {
|
||||
'use memo if(true)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
2 |
|
||||
3 | function Foo() {
|
||||
> 4 | 'use memo if(true)';
|
||||
| ^^^^^^^^^^^^^^^^^^^^ InvalidReact: Dynamic gating directive is not a valid JavaScript identifier. Found 'use memo if(true)' (4:4)
|
||||
5 | return <div>hello world</div>;
|
||||
6 | }
|
||||
7 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"}
|
||||
|
||||
function Foo() {
|
||||
'use memo if(true)';
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
const x = props.foo
|
||||
? 1
|
||||
: (() => {
|
||||
throw new Error('Did not receive 1');
|
||||
})();
|
||||
return items;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
props.foo ? 1 : _temp();
|
||||
return items;
|
||||
}
|
||||
function _temp() {
|
||||
throw new Error("Did not receive 1");
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ foo: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) items is not defined
|
||||
@@ -0,0 +1,13 @@
|
||||
function Component(props) {
|
||||
const x = props.foo
|
||||
? 1
|
||||
: (() => {
|
||||
throw new Error('Did not receive 1');
|
||||
})();
|
||||
return items;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: true}],
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @inferEffectDependencies @panicThreshold:"none"
|
||||
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
|
||||
/**
|
||||
* TODO: run the non-forget enabled version through the effect inference
|
||||
* pipeline.
|
||||
*/
|
||||
function Component({foo}) {
|
||||
'use memo if(getTrue)';
|
||||
const arr = [];
|
||||
useEffectWrapper(() => arr.push(foo));
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 1}],
|
||||
sequentialRenders: [{foo: 1}, {foo: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
10 | 'use memo if(getTrue)';
|
||||
11 | const arr = [];
|
||||
> 12 | useEffectWrapper(() => arr.push(foo));
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (12:12)
|
||||
13 | arr.push(2);
|
||||
14 | return arr;
|
||||
15 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"} @inferEffectDependencies @panicThreshold:"none"
|
||||
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
|
||||
/**
|
||||
* TODO: run the non-forget enabled version through the effect inference
|
||||
* pipeline.
|
||||
*/
|
||||
function Component({foo}) {
|
||||
'use memo if(getTrue)';
|
||||
const arr = [];
|
||||
useEffectWrapper(() => arr.push(foo));
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 1}],
|
||||
sequentialRenders: [{foo: 1}, {foo: 2}],
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @gating @inferEffectDependencies @panicThreshold:"none"
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
|
||||
/**
|
||||
* TODO: run the non-forget enabled version through the effect inference
|
||||
* pipeline.
|
||||
*/
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
useEffectWrapper(() => arr.push(foo));
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 1}],
|
||||
sequentialRenders: [{foo: 1}, {foo: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
8 | function Component({foo}) {
|
||||
9 | const arr = [];
|
||||
> 10 | useEffectWrapper(() => arr.push(foo));
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (10:10)
|
||||
11 | arr.push(2);
|
||||
12 | return arr;
|
||||
13 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// @gating @inferEffectDependencies @panicThreshold:"none"
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
|
||||
/**
|
||||
* TODO: run the non-forget enabled version through the effect inference
|
||||
* pipeline.
|
||||
*/
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
useEffectWrapper(() => arr.push(foo));
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 1}],
|
||||
sequentialRenders: [{foo: 1}, {foo: 2}],
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
// Taking either arr[0].value or arr as a dependency is reasonable
|
||||
// as long as developers know what to expect.
|
||||
useEffect(() => print(arr[0]?.value));
|
||||
arr.push({value: foo});
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
|
||||
import { useEffect } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const { foo } = t0;
|
||||
const arr = [];
|
||||
|
||||
useEffect(() => print(arr[0]?.value), [arr[0]?.value]);
|
||||
arr.push({ value: foo });
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ foo: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":10,"column":2,"index":345},"end":{"line":10,"column":5,"index":348},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}}
|
||||
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":9,"column":2,"index":304},"end":{"line":9,"column":39,"index":341},"filename":"mutate-after-useeffect-optional-chain.ts"},"decorations":[{"start":{"line":9,"column":24,"index":326},"end":{"line":9,"column":27,"index":329},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}]}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [{"value":1}]
|
||||
logs: [1]
|
||||
@@ -0,0 +1,17 @@
|
||||
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
// Taking either arr[0].value or arr as a dependency is reasonable
|
||||
// as long as developers know what to expect.
|
||||
useEffect(() => print(arr[0]?.value));
|
||||
arr.push({value: foo});
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 1}],
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold:"none"
|
||||
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
|
||||
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
@@ -14,12 +14,17 @@ function Component({arrRef}) {
|
||||
return arrRef;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{arrRef: {current: {val: 'initial ref value'}}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold:"none"
|
||||
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
@@ -32,7 +37,21 @@ function Component(t0) {
|
||||
return arrRef;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ arrRef: { current: { val: "initial ref value" } } }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"reason":"Mutating component props or hook arguments is not allowed. Consider using a local variable instead","description":null,"loc":{"start":{"line":9,"column":2,"index":269},"end":{"line":9,"column":16,"index":283},"filename":"mutate-after-useeffect-ref-access.ts"},"suggestions":null,"severity":"InvalidReact"}}
|
||||
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":227},"end":{"line":8,"column":40,"index":265},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":249},"end":{"line":8,"column":30,"index":255},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
(kind: ok) {"current":{"val":2}}
|
||||
logs: [{ val: 2 }]
|
||||
@@ -1,4 +1,4 @@
|
||||
// @inferEffectDependencies @panicThreshold:"none"
|
||||
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
|
||||
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
@@ -9,3 +9,8 @@ function Component({arrRef}) {
|
||||
arrRef.current.val = 2;
|
||||
return arrRef;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{arrRef: {current: {val: 'initial ref value'}}}],
|
||||
};
|
||||
|
||||
@@ -2,33 +2,55 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold:"none"
|
||||
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
useEffect(() => arr.push(foo));
|
||||
useEffect(() => {
|
||||
arr.push(foo);
|
||||
});
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold:"none"
|
||||
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
|
||||
import { useEffect } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const { foo } = t0;
|
||||
const arr = [];
|
||||
useEffect(() => arr.push(foo), [arr, foo]);
|
||||
useEffect(() => {
|
||||
arr.push(foo);
|
||||
}, [arr, foo]);
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ foo: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":9,"column":2,"index":194},"end":{"line":9,"column":5,"index":197},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}}
|
||||
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":6,"column":2,"index":149},"end":{"line":8,"column":4,"index":190},"filename":"mutate-after-useeffect.ts"},"decorations":[{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":13,"index":180},"end":{"line":7,"column":16,"index":183},"filename":"mutate-after-useeffect.ts","identifierName":"foo"}]}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
(kind: ok) [2]
|
||||
@@ -1,9 +1,16 @@
|
||||
// @inferEffectDependencies @panicThreshold:"none"
|
||||
// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
useEffect(() => arr.push(foo));
|
||||
useEffect(() => {
|
||||
arr.push(foo);
|
||||
});
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 1}],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect, useEffectEvent} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* We do not include effect events in dep arrays.
|
||||
*/
|
||||
function NonReactiveEffectEvent() {
|
||||
const fn = useEffectEvent(() => print('hello world'));
|
||||
useEffect(() => fn());
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect, useEffectEvent } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* We do not include effect events in dep arrays.
|
||||
*/
|
||||
function NonReactiveEffectEvent() {
|
||||
const $ = _c(2);
|
||||
const fn = useEffectEvent(_temp);
|
||||
let t0;
|
||||
if ($[0] !== fn) {
|
||||
t0 = () => fn();
|
||||
$[0] = fn;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
useEffect(t0, []);
|
||||
}
|
||||
function _temp() {
|
||||
return print("hello world");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect, useEffectEvent} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* We do not include effect events in dep arrays.
|
||||
*/
|
||||
function NonReactiveEffectEvent() {
|
||||
const fn = useEffectEvent(() => print('hello world'));
|
||||
useEffect(() => fn());
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print, shallowCopy} from 'shared-runtime';
|
||||
|
||||
function ReactiveMemberExpr({cond, propVal}) {
|
||||
const obj = {a: cond ? {b: propVal} : null, c: null};
|
||||
const other = shallowCopy({a: {b: {c: {d: {e: {f: propVal + 1}}}}}});
|
||||
const primitive = shallowCopy(propVal);
|
||||
useEffect(() =>
|
||||
print(obj.a?.b, other?.a?.b?.c?.d?.e.f, primitive.a?.b.c?.d?.e.f)
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ReactiveMemberExpr,
|
||||
params: [{cond: true, propVal: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect } from "react";
|
||||
import { print, shallowCopy } from "shared-runtime";
|
||||
|
||||
function ReactiveMemberExpr(t0) {
|
||||
const $ = _c(13);
|
||||
const { cond, propVal } = t0;
|
||||
let t1;
|
||||
if ($[0] !== cond || $[1] !== propVal) {
|
||||
t1 = cond ? { b: propVal } : null;
|
||||
$[0] = cond;
|
||||
$[1] = propVal;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = { a: t1, c: null };
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const obj = t2;
|
||||
const t3 = propVal + 1;
|
||||
let t4;
|
||||
if ($[5] !== t3) {
|
||||
t4 = shallowCopy({ a: { b: { c: { d: { e: { f: t3 } } } } } });
|
||||
$[5] = t3;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
const other = t4;
|
||||
let t5;
|
||||
if ($[7] !== propVal) {
|
||||
t5 = shallowCopy(propVal);
|
||||
$[7] = propVal;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
const primitive = t5;
|
||||
let t6;
|
||||
if (
|
||||
$[9] !== obj.a?.b ||
|
||||
$[10] !== other?.a?.b?.c?.d?.e.f ||
|
||||
$[11] !== primitive.a?.b.c?.d?.e.f
|
||||
) {
|
||||
t6 = () =>
|
||||
print(obj.a?.b, other?.a?.b?.c?.d?.e.f, primitive.a?.b.c?.d?.e.f);
|
||||
$[9] = obj.a?.b;
|
||||
$[10] = other?.a?.b?.c?.d?.e.f;
|
||||
$[11] = primitive.a?.b.c?.d?.e.f;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
useEffect(t6, [obj.a?.b, other?.a?.b?.c?.d?.e.f, primitive.a?.b.c?.d?.e.f]);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ReactiveMemberExpr,
|
||||
params: [{ cond: true, propVal: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok)
|
||||
logs: [1,2,undefined]
|
||||
@@ -0,0 +1,17 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print, shallowCopy} from 'shared-runtime';
|
||||
|
||||
function ReactiveMemberExpr({cond, propVal}) {
|
||||
const obj = {a: cond ? {b: propVal} : null, c: null};
|
||||
const other = shallowCopy({a: {b: {c: {d: {e: {f: propVal + 1}}}}}});
|
||||
const primitive = shallowCopy(propVal);
|
||||
useEffect(() =>
|
||||
print(obj.a?.b, other?.a?.b?.c?.d?.e.f, primitive.a?.b.c?.d?.e.f)
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ReactiveMemberExpr,
|
||||
params: [{cond: true, propVal: 1}],
|
||||
};
|
||||
@@ -6,12 +6,17 @@
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
// TODO: take optional chains as dependencies
|
||||
function ReactiveMemberExpr({cond, propVal}) {
|
||||
const obj = {a: cond ? {b: propVal} : null};
|
||||
const obj = {a: cond ? {b: propVal} : null, c: null};
|
||||
useEffect(() => print(obj.a?.b));
|
||||
useEffect(() => print(obj.c?.d));
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ReactiveMemberExpr,
|
||||
params: [{cond: true, propVal: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
@@ -21,9 +26,8 @@ import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
// TODO: take optional chains as dependencies
|
||||
function ReactiveMemberExpr(t0) {
|
||||
const $ = _c(7);
|
||||
const $ = _c(9);
|
||||
const { cond, propVal } = t0;
|
||||
let t1;
|
||||
if ($[0] !== cond || $[1] !== propVal) {
|
||||
@@ -36,7 +40,7 @@ function ReactiveMemberExpr(t0) {
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = { a: t1 };
|
||||
t2 = { a: t1, c: null };
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
@@ -51,10 +55,25 @@ function ReactiveMemberExpr(t0) {
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
useEffect(t3, [obj.a]);
|
||||
useEffect(t3, [obj.a?.b]);
|
||||
let t4;
|
||||
if ($[7] !== obj.c?.d) {
|
||||
t4 = () => print(obj.c?.d);
|
||||
$[7] = obj.c?.d;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
useEffect(t4, [obj.c?.d]);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ReactiveMemberExpr,
|
||||
params: [{ cond: true, propVal: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
(kind: ok)
|
||||
logs: [1,undefined]
|
||||
@@ -2,8 +2,13 @@
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
// TODO: take optional chains as dependencies
|
||||
function ReactiveMemberExpr({cond, propVal}) {
|
||||
const obj = {a: cond ? {b: propVal} : null};
|
||||
const obj = {a: cond ? {b: propVal} : null, c: null};
|
||||
useEffect(() => print(obj.a?.b));
|
||||
useEffect(() => print(obj.c?.d));
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: ReactiveMemberExpr,
|
||||
params: [{cond: true, propVal: 1}],
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ const ComponentWithHookInsideCallback = React.forwardRef((props, ref) => {
|
||||
6 | const ComponentWithHookInsideCallback = React.forwardRef((props, ref) => {
|
||||
7 | useEffect(() => {
|
||||
> 8 | useHookInsideCallback();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (8:8)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (8:8)
|
||||
9 | });
|
||||
10 | return <button {...props} ref={ref} />;
|
||||
11 | });
|
||||
|
||||
@@ -23,7 +23,7 @@ const ComponentWithHookInsideCallback = React.memo(props => {
|
||||
6 | const ComponentWithHookInsideCallback = React.memo(props => {
|
||||
7 | useEffect(() => {
|
||||
> 8 | useHookInsideCallback();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (8:8)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (8:8)
|
||||
9 | });
|
||||
10 | return <button {...props} />;
|
||||
11 | });
|
||||
|
||||
@@ -31,7 +31,7 @@ function Component() {
|
||||
8 | const y = {
|
||||
9 | inner() {
|
||||
> 10 | return useFoo();
|
||||
| ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (10:10)
|
||||
| ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (10:10)
|
||||
11 | },
|
||||
12 | };
|
||||
13 | return y;
|
||||
|
||||
@@ -27,7 +27,7 @@ function Component() {
|
||||
6 | const y = {
|
||||
7 | inner() {
|
||||
> 8 | return useFoo();
|
||||
| ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (8:8)
|
||||
| ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (8:8)
|
||||
9 | },
|
||||
10 | };
|
||||
11 | return y;
|
||||
|
||||
@@ -21,7 +21,7 @@ function createHook() {
|
||||
4 | return function useHookWithConditionalHook() {
|
||||
5 | if (cond) {
|
||||
> 6 | useConditionalHook();
|
||||
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (6:6)
|
||||
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (6:6)
|
||||
7 | }
|
||||
8 | };
|
||||
9 | }
|
||||
|
||||
@@ -21,9 +21,9 @@ function createComponent() {
|
||||
4 | return function ComponentWithHookInsideCallback() {
|
||||
5 | useEffect(() => {
|
||||
> 6 | useHookInsideCallback();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (6:6)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (6:6)
|
||||
|
||||
InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function component (5:5)
|
||||
InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function expression (5:5)
|
||||
7 | });
|
||||
8 | };
|
||||
9 | }
|
||||
|
||||
@@ -21,7 +21,7 @@ function createComponent() {
|
||||
4 | return function ComponentWithHookInsideCallback() {
|
||||
5 | function handleClick() {
|
||||
> 6 | useState();
|
||||
| ^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useState within a function component (6:6)
|
||||
| ^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useState within a function expression (6:6)
|
||||
7 | }
|
||||
8 | };
|
||||
9 | }
|
||||
|
||||
@@ -19,7 +19,7 @@ function ComponentWithHookInsideCallback() {
|
||||
3 | function ComponentWithHookInsideCallback() {
|
||||
4 | function handleClick() {
|
||||
> 5 | useState();
|
||||
| ^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useState within a function component (5:5)
|
||||
| ^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useState within a function expression (5:5)
|
||||
6 | }
|
||||
7 | }
|
||||
8 |
|
||||
|
||||
@@ -21,7 +21,7 @@ function createComponent() {
|
||||
4 | return function ComponentWithConditionalHook() {
|
||||
5 | if (cond) {
|
||||
> 6 | useConditionalHook();
|
||||
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (6:6)
|
||||
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (6:6)
|
||||
7 | }
|
||||
8 | };
|
||||
9 | }
|
||||
|
||||
@@ -19,7 +19,7 @@ function ComponentWithHookInsideCallback() {
|
||||
3 | function ComponentWithHookInsideCallback() {
|
||||
4 | useEffect(() => {
|
||||
> 5 | useHookInsideCallback();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (5:5)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (5:5)
|
||||
6 | });
|
||||
7 | }
|
||||
8 |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
```javascript
|
||||
function ternary(props) {
|
||||
const a = props.a && props.b ? props.c || props.d : props.e ?? props.f;
|
||||
const a = props.a && props.b ? props.c || props.d : (props.e ?? props.f);
|
||||
const b = props.a ? (props.b && props.c ? props.d : props.e) : props.f;
|
||||
return a ? b : null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
function ternary(props) {
|
||||
const a = props.a && props.b ? props.c || props.d : props.e ?? props.f;
|
||||
const a = props.a && props.b ? props.c || props.d : (props.e ?? props.f);
|
||||
const b = props.a ? (props.b && props.c ? props.d : props.e) : props.f;
|
||||
return a ? b : null;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ function Component(props) {
|
||||
7 | };
|
||||
8 | useEffect(() => {
|
||||
> 9 | useEffect(() => {
|
||||
| ^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function component (9:9)
|
||||
| ^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function expression (9:9)
|
||||
10 | function nested() {
|
||||
11 | fire(foo(props));
|
||||
12 | }
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
function Component(props) {
|
||||
return (
|
||||
useMemo(() => {
|
||||
return [props.value];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.value) {
|
||||
t0 = (() => [props.value])() || [];
|
||||
$[0] = props.value;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [1]
|
||||
@@ -0,0 +1,13 @@
|
||||
import {useMemo} from 'react';
|
||||
function Component(props) {
|
||||
return (
|
||||
useMemo(() => {
|
||||
return [props.value];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 1}],
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export {
|
||||
OPT_OUT_DIRECTIVES,
|
||||
OPT_IN_DIRECTIVES,
|
||||
ProgramContext,
|
||||
findDirectiveEnablingMemoization,
|
||||
tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization,
|
||||
findDirectiveDisablingMemoization,
|
||||
type CompilerPipelineValue,
|
||||
type Logger,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {queryAlgolia} from './utils/algolia';
|
||||
import assertExhaustive from './utils/assertExhaustive';
|
||||
import {convert} from 'html-to-text';
|
||||
import {measurePerformance} from './tools/runtimePerf';
|
||||
import {parseReactComponentTree} from './tools/componentTree';
|
||||
|
||||
function calculateMean(values: number[]): string {
|
||||
return values.length > 0
|
||||
@@ -366,6 +367,45 @@ ${calculateMean(results.renderTime)}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'parse-react-component-tree',
|
||||
`
|
||||
This tool gets the component tree of a React App.
|
||||
passing in a url will attempt to connect to the browser and get the current state of the component tree. If no url is passed in,
|
||||
the default url will be used (http://localhost:3000).
|
||||
|
||||
<requirements>
|
||||
- The url should be a full url with the protocol (http:// or https://) and the domain name (e.g. localhost:3000).
|
||||
- Also the user should be running a Chrome browser running on debug mode on port 9222. If you receive an error message, advise the user to run
|
||||
the following comand in the terminal:
|
||||
MacOS: "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome"
|
||||
Windows: "chrome.exe --remote-debugging-port=9222 --user-data-dir=C:\temp\chrome"
|
||||
</requirements>
|
||||
`,
|
||||
{
|
||||
url: z.string().optional().default('http://localhost:3000'),
|
||||
},
|
||||
async ({url}) => {
|
||||
try {
|
||||
const componentTree = await parseReactComponentTree(url);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: componentTree,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.prompt('review-react-code', () => ({
|
||||
messages: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
export async function parseReactComponentTree(url: string): Promise<string> {
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: 'http://127.0.0.1:9222',
|
||||
defaultViewport: null,
|
||||
});
|
||||
|
||||
const pages = await browser.pages();
|
||||
|
||||
let localhostPage = null;
|
||||
for (const page of pages) {
|
||||
const pageUrl = await page.url();
|
||||
|
||||
if (pageUrl.startsWith(url)) {
|
||||
localhostPage = page;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (localhostPage) {
|
||||
const componentTree = await localhostPage.evaluate(() => {
|
||||
return (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces
|
||||
.get(1)
|
||||
.__internal_only_getComponentTree();
|
||||
});
|
||||
|
||||
return componentTree;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Could not open the page at ${url}. Is your server running?`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Failed extract component tree' + error);
|
||||
}
|
||||
}
|
||||
@@ -460,6 +460,7 @@ const skipFilter = new Set([
|
||||
'fbt/bug-fbt-plural-multiple-function-calls',
|
||||
'fbt/bug-fbt-plural-multiple-mixed-call-tag',
|
||||
'bug-invalid-phi-as-dependency',
|
||||
'bug-ref-prefix-postfix-operator',
|
||||
|
||||
// 'react-compiler-runtime' not yet supported
|
||||
'flag-enable-emit-hook-guards',
|
||||
|
||||
@@ -242,6 +242,7 @@ export async function transformFixtureInput(
|
||||
filename: virtualFilepath,
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
compact: true,
|
||||
plugins: [
|
||||
[plugin, options],
|
||||
'babel-plugin-fbt',
|
||||
|
||||
@@ -128,6 +128,14 @@ export function getNull(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getTrue(): true {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getFalse(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function calculateExpensiveNumber(x: number): number {
|
||||
return x;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
# Executes rustfmt using a nightly build of the compiler
|
||||
# NOTE: this command must exactly match the Rust Lint command in .github/workflows/rust.yml
|
||||
rustup toolchain list | grep -q nightly-2023-08-01 || (echo "Expected Rust version missing, try running: 'rustup toolchain install nightly-2023-08-01'" && exit 1)
|
||||
grep -r --include "*.rs" --files-without-match "@generated" crates | xargs rustup run nightly-2023-08-01 rustfmt --config="skip_children=true" "$@"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user